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
22 changes: 22 additions & 0 deletions apps/docs/content/docs/(index)/next/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@ Start with the setup page when you want a guided first run.
</Card>
</Cards>

## Learn the fundamentals

Once you are connected, the Fundamentals section teaches the everyday query patterns.

<Cards>
<Card href="/orm/next/fundamentals/reading-data" title="Reading data" icon={<BookOpen className="text-primary" />}>
Filter with where, project with select, sort, and paginate.
</Card>
<Card href="/orm/next/fundamentals/writing-data" title="Writing data" icon={<Pencil className="text-primary" />}>
Create, update, delete, upsert, and the bulk write variants.
</Card>
<Card href="/orm/next/fundamentals/relations-and-joins" title="Relations and joins" icon={<Network className="text-primary" />}>
Read related records with include on PostgreSQL and MongoDB.
</Card>
<Card href="/orm/next/fundamentals/transactions" title="Transactions" icon={<Layers className="text-primary" />}>
Make several writes succeed or fail together.
</Card>
<Card href="/orm/next/fundamentals/advanced-queries" title="Advanced queries" icon={<Terminal className="text-primary" />}>
The SQL builder and the MongoDB pipeline builder for shapes the ORM can't express.
</Card>
</Cards>

## Learn the concepts

<Cards>
Expand Down
142 changes: 142 additions & 0 deletions apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
title: Advanced queries
description: Use the SQL builder on PostgreSQL and the pipeline builder on MongoDB for queries the ORM can't express.
url: /orm/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, or the pipeline builder on MongoDB. Both stay typed against your contract; neither means writing raw strings.

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

## Query with the SQL builder (PostgreSQL)

Use `db.sql.public.<table>` to build a query as a *plan*, then execute the plan through the runtime. Tables use their lowercase storage names:

```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 publishedPosts = await db.runtime().execute(plan);
```

The `.where(...)` callback receives `(fields, fns)`: `fields` holds the column references, `fns` the operators (`eq`, `ne`, `gt`, `lt`, `and`, `count`, and operators added by extensions).

### Join tables explicitly

Use the SQL builder when you need a join the ORM doesn't express. Alias each side with `.as(...)`, join on any condition, and project columns from both sides:

```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 postsWithAuthors = await db.runtime().execute(plan);
```

This is also how you read through a many-to-many junction table while [`.include(...)` does not support it](/orm/next/fundamentals/relations-and-joins#current-limitations): join the junction table to the far side explicitly.

### Group and rank results

Use the SQL builder for "top N groups" questions, such as the authors with the most posts. It can order and limit by an aggregate directly in the database, which the ORM's grouped aggregates can't:

```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 topAuthors = await db.runtime().execute(plan);
// PostgreSQL returns counts as strings; convert with Number(row.posts)
```

### Write with RETURNING

SQL builder writes take an array of rows. Use `.returning(...)` to choose which columns come back:

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

const [insertedUser] = await db.runtime().execute(plan);
// Contract defaults such as generated IDs are applied
```

Prisma Next does not run raw SQL strings: every query goes through the typed builder. If the builder can't express a shape you need, [share the use case](https://pris.ly/discord).

## Query with the pipeline builder (MongoDB)

Use `db.query.from("<collection>")` to build a typed aggregation pipeline, then execute it through the runtime. This is also where MongoDB aggregation lives, since the MongoDB ORM has no `.aggregate(...)`:

```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 postsByAuthor = await runtime.execute(plan);
```

Use `.match(...)` to filter with typed field accessors, and `.lookup(...)` for a type-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 postsWithAuthors = await runtime.execute(plan);
```

Accumulators such as `acc.count()` and `acc.max(...)` import from `@prisma-next/mongo-query-builder`.

## Choose the right lane

| 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 and `(await db.runtime()).execute(plan)` on MongoDB. Inside a [transaction](/orm/next/fundamentals/transactions), use `tx.execute(plan)`.

## Next

- [Read data](/orm/next/fundamentals/reading-data): the ORM happy path these builders back up.
- [Run SQL builder plans atomically](/orm/next/fundamentals/transactions) inside a transaction.
10 changes: 10 additions & 0 deletions apps/docs/content/docs/orm/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"
]
}
143 changes: 143 additions & 0 deletions apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
title: Reading data
description: Fetch one record or many with the Prisma Next ORM, then filter, select, sort, and paginate the results.
url: /orm/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, then running the chain with `.all()` for many records or `.first()` for one.

```typescript tab="PostgreSQL"
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();
```

```typescript tab="MongoDB"
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();
```

Every result is typed against your contract. Models are addressed by schema namespace on PostgreSQL (`db.orm.public.User`, where `public` is the default PostgreSQL schema) and by collection name on MongoDB (`db.orm.users`).

If you are coming from Prisma 7: `.where(...).all()` replaces `findMany`, and `.where(...).first()` replaces `findFirst` and `findUnique`.

## Fetch many records or one

Use `.all()` when you want every matching record. It returns an array and applies no limit of its own, so combine it with [`take`](#sort-and-paginate) on tables that can grow.

Use `.first()` when you want a single record. It returns the record or `null`, and on PostgreSQL it fetches at most one row. For a primary-key lookup on PostgreSQL, pass the key directly:

```typescript
const user = await db.orm.public.User.first({ id: userId });
```

On MongoDB, look up documents by their `_id` field: `db.orm.users.where({ _id: id }).first()`.

## Filter records

Use `.where(...)` to narrow a query. Pass an object to match fields by equality:

```typescript
const drafts = await db.orm.public.Post.where({ published: false }).all();
```

Chain several `.where(...)` calls to combine conditions with AND. This is also how you express a range:

```typescript
const recentPosts = await db.orm.public.Post
.where((p) => p.createdAt.gte(start))
.where((p) => p.createdAt.lte(end))
.all();
```

On PostgreSQL, `.where(...)` also accepts a lambda for richer comparisons, as in the range example above. The field proxy supports `.eq`, `.neq`, `.lt`, `.lte`, `.gt`, `.gte`, `.like`, `.ilike`, `.in([...])`, `.isNull()`, and `.isNotNull()`:

```typescript
const matchingPosts = await db.orm.public.Post
.where((p) => p.title.ilike("%prisma%"))
.all();
```

To combine conditions with OR or NOT on PostgreSQL, use the `or`, `and`, and `not` helpers, currently exported from `@prisma-next/sql-orm-client`.

On MongoDB, use the object form. Comparison operators and boolean logic are not available on MongoDB's `.where(...)` yet; for those queries, use the [pipeline builder](/orm/next/fundamentals/advanced-queries#query-with-the-pipeline-builder-mongodb).

## Select fields

Use `.select(...)` to fetch only the fields you need. The result type narrows to match:

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

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

## Sort and paginate

Use `.orderBy(...)` to sort, `.take(n)` to limit, and `.skip(n)` to offset:

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

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

PostgreSQL sorts with lambdas calling `.asc()` or `.desc()`; pass an array of lambdas for a composite sort. MongoDB sorts with the driver's direction values: `1` for ascending, `-1` for descending.

For stable pagination over large tables on PostgreSQL, follow `.orderBy(...)` with `.cursor({ createdAt: last.createdAt })` to resume from a known position instead of counting offsets.

## Count records

Count through `.aggregate(...)` on PostgreSQL. It returns an object with the keys you name:

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

There is no `.count()` method on the query chain. On MongoDB, count with a `$group` stage in the [pipeline builder](/orm/next/fundamentals/advanced-queries#query-with-the-pipeline-builder-mongodb).

## Common mistakes

:::warning

A query result can only be consumed once. `await` buffers it into an array; a second `await` on the same result throws `RUNTIME.ITERATOR_CONSUMED`. Store the array in a variable and reuse the variable.

:::

- Using `.all()` for a single record. Use `.first()`: it returns one record or `null`, without fetching the rest.
- Expecting `.all()` to limit itself. It returns every match; add `.take(n)` when the table can grow.
- Loading a huge result into memory. Iterate it instead of awaiting: `for await (const row of query.all())`.

## Next

- [Write data](/orm/next/fundamentals/writing-data): create, update, delete, and upsert records.
- [Read related records](/orm/next/fundamentals/relations-and-joins) in the same query with `.include(...)`.
- [Use advanced queries](/orm/next/fundamentals/advanced-queries) when a shape needs the SQL builder or a MongoDB pipeline.
Loading
Loading