Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/skills/docs-writer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ If a rule here conflicts with house style, follow the house style and flag the c

For a step-by-step example of writing each kind of page (how-to, concept, reference) and rewriting an existing one, see `references/how-to-use.md`.

For a Prisma Next docs page or section (anything under `content/docs/orm/next/` or `content/docs/(index)/next/`), also read `references/prisma-next.md`: page location, redirect handling (commented out until the URL cutover), tested-example requirements, tab and diagram conventions, and naming rules.

## Foundation

These come from Prisma's positioning. They shape how docs frame the product, without turning a page into marketing.
Expand Down
40 changes: 40 additions & 0 deletions .claude/skills/docs-writer/references/prisma-next.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Prisma Next docs conventions

Follow these when writing or reviewing a Prisma Next docs section in `apps/docs` (the Fundamentals, Middleware, Extensions, Reference, and Guides trees). They encode decisions from DR-8681/DR-8687/DR-8688 and PR #8011 review rounds so parallel section PRs stay consistent.

## Where pages live

- Concept and task docs go in the ORM "Next" version tree: `content/docs/orm/next/<section>/` served at `/docs/orm/next/<section>/<slug>`. The version dropdown keys off the `/orm/next/*` path.
- Getting-started funnel pages stay under `content/docs/(index)/next/`.
- Frontmatter `url` must mirror the file path. Register the section in `content/docs/orm/next/meta.json`.

## Redirects: commented out until the cutover

Do not add live Prisma 7 → Prisma Next redirects yet. They retire live URLs, and they ship together when the `/orm/next` tree becomes `/orm` (DR-8687).

Instead, append your section's redirect map to the commented block in `apps/docs/next.config.mjs` (search for "Prisma Next URL cutover"). Keep the entries commented, one per line, `permanent: false`, with your DR reference. Pages with no Prisma Next equivalent stay live on the Prisma 7 tree; list them in the same comment block for the SEO owner.

## Accuracy: test before you write

Every code sample must be executed against the published `@prisma-next` packages before it lands, or clearly marked as conceptual. Scaffold throwaway apps with `create-prisma@next` non-interactive flags; use `bunx create-db` for PostgreSQL and `mongodb-memory-server` (replica set) for MongoDB. Key tested facts that older internal docs get wrong:

- PostgreSQL model access is namespace-qualified: `db.orm.public.User`, `db.sql.public.user`. MongoDB uses flat lowercase plural roots (`db.orm.users`) and documents keep `_id`.
- `.update()` / `.delete()` affect one record; `updateAll` / `deleteAll` / `*Count` are the bulk forms. No `.count()` terminal; use `.aggregate(...)`.
- No `data` wrapper on `.create(...)`. SQL-builder inserts take an array of rows.
- MongoDB: no `db.transaction(...)`, no ORM `.aggregate(...)`, `@default(now())` not applied at create time.
- Raw SQL exists only as `fns.raw` fragments inside the SQL query builder, not as standalone statements.

## Page shape

- Task-first sections: what the user does, which API, simplest example, what it returns, caveats last. Result shapes go in ` ```js no-copy ` blocks after the query.
- Database variants use adjacent code fences with `tab="PostgreSQL"` / `tab="MongoDB"` (PostgreSQL first). Never use the `<Tabs items=...>` JSX in mdx pages; it renders broken.
- Open with a section-linked intro; add an expandable `<details>` example schema when the page's examples depend on one.
- Prisma 7 migration notes are ` ```diff ` blocks, framed as help, never required context.
- Diagrams: add a `FlowScene` to `apps/docs/src/components/concept-animation/flow-presets.ts` and render with `<ConceptAnimation name="..." />`. Design as Mermaid first in the PR description.
- End each page with a "Prompt your coding agent" section: copyable prompts per section referencing the scaffolded skills (`prisma-next-queries`, `prisma-next-contract`, ...), then "Next" links.

## Naming and claims

- The product is "Prisma Next", not "Prisma Next ORM". The high-level query lane is "the ORM API"; the low-level lanes are "the SQL query builder" and "the pipeline builder".
- No em dashes. Be honest about Early Access; state limitations in user-facing language ("X is not supported yet. For now, do Y") and show the tested workaround.
- Validate with `pnpm --filter docs types:check`, `pnpm lint:links`, `pnpm exec cspell --no-progress "content/docs/orm/next/**/*.mdx"` (use placeholder ids in result blocks; random cuid fragments fail cspell), and a dev-server smoke test.
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
245 changes: 245 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,245 @@
---
title: Advanced queries
description: Use the SQL query builder on PostgreSQL and the pipeline builder on MongoDB for queries the ORM API can't express.
url: /orm/next/fundamentals/advanced-queries
metaTitle: Advanced queries in Prisma Next
metaDescription: Use the Prisma Next SQL query builder for explicit joins, grouped aggregates, and RETURNING, and the typed aggregation pipeline builder on MongoDB.
---

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

The choice is per query, not per app. A codebase that uses the ORM API everywhere and the builders in three hot spots is the intended shape.

## PostgreSQL: SQL query builder

The SQL query builder composes a single SQL statement as a typed *plan*: a description of the query you build once and execute through the runtime. You keep full control over the SQL shape (joins, grouping, projections) and full type safety against your contract.

**Use it when:**

- The query is easier to say in SQL: joins with conditions, computed columns, set-shaped results.
- You need PostgreSQL behavior the ORM API doesn't surface, such as `RETURNING` on a bulk insert.
- An aggregation needs precise control, like ordering and limiting by an aggregate in the database.
- A query is performance-sensitive and you want to decide its exact shape.

**Prefer the ORM API when** the query is CRUD, filtered reads, or relation traversal. The [reading](/orm/next/fundamentals/reading-data), [writing](/orm/next/fundamentals/writing-data), and [relations](/orm/next/fundamentals/relations-and-joins) pages cover that surface, with less code and the same type safety.

### Build and run a plan

Start from a table with `db.sql.public.<table>` (tables use lowercase storage names), chain clauses, and call `.build()`. Execute the plan with 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 publishedPosts = await db.runtime().execute(plan);
```

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

### Join tables with precise control

Alias each side with `.as(...)`, join on any condition, and project columns from both sides into a flat result:

```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);
// Array<{ postId, title, authorEmail }>
```

Chain more joins for multi-hop traversals. This is how you get a flat post-tag list through a [many-to-many junction table](/orm/next/fundamentals/relations-and-joins#many-to-many), one row per pair:

```typescript
const plan = db.sql.public.postTag
.as("pt")
.innerJoin(db.sql.public.tag.as("t"), (f, fns) => fns.eq(f.pt.tagId, f.t.id))
.innerJoin(db.sql.public.post.as("p"), (f, fns) => fns.eq(f.pt.postId, f.p.id))
.select((f) => ({ postTitle: f.p.title, tagName: f.t.name }))
.build();

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

```js no-copy
[
{ postTitle: 'Hello Prisma Next', tagName: 'databases' },
{ postTitle: 'Hello Prisma Next', tagName: 'typescript' },
{ postTitle: 'Typed queries', tagName: 'typescript' }
]
```

### Group and rank results

Answer "top N groups" questions, such as the authors with the most posts, by ordering and limiting on an aggregate directly in the database:

```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);
```

```js no-copy
[
{ authorId: 'cuid20000000000000000001', posts: '2' },
{ authorId: 'cuid20000000000000000002', posts: '1' }
]
```

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 from the same statement:

```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
```

### Raw SQL fragments

Prisma Next does not run standalone raw SQL statements: every query goes through the typed builder. When the operators don't cover an expression you need, embed a raw fragment with `fns.raw` and declare its type with `.returns(...)`. The rest of the query stays typed:

```typescript
const plan = db.sql.public.user
.select("id", "email")
.select("upperEmail", (f, fns) => fns.raw`UPPER(${f.email})`.returns("pg/text@1"))
.limit(10)
.build();

const users = await db.runtime().execute(plan);
// [{ id: 'cuid20000000000000000001', email: 'alice@prisma.io', upperEmail: 'ALICE@PRISMA.IO' }, ...]
```

Interpolated values are AST nodes, not string splices, so a fragment can reference columns and other typed expressions safely. If the builder plus `fns.raw` still can't express a shape you need, [share the use case](https://pris.ly/discord).

## MongoDB: Pipeline builder

The pipeline builder composes a typed MongoDB aggregation pipeline: a sequence of stages such as `$match`, `$group`, `$sort`, and `$lookup`, checked against your contract. It is the MongoDB counterpart of the SQL query builder, and it is also where all MongoDB aggregation lives, because the ORM API has no `.aggregate(...)` on MongoDB.

**Use it when:**

- You need an aggregation: counts, grouping, or summaries per key.
- You want to join and reshape documents across collections with `$lookup`.
- A query needs MongoDB pipeline stages that don't map to the ORM API, such as multi-stage filtering and projection.
- You need operators the MongoDB `.where(...)` doesn't cover yet, like ranges or boolean logic.

**Prefer the ORM API when** the query is document CRUD or a reference-relation read; `.include(...)` already covers the common `$lookup` case.

### Build and run a pipeline

Start from a collection with `db.query.from(...)`, chain stages, and call `.build()`. Execute the plan through the runtime:

```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);
```

```js no-copy
[
{ _id: new ObjectId('650000000000000000000001'), postCount: 3 },
{ _id: new ObjectId('650000000000000000000002'), postCount: 2 }
]
```

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

### Filter and group in stages

Chain `.match(...)` before `.group(...)` to aggregate over a subset, the pipeline equivalent of `WHERE` before `GROUP BY`:

```typescript
const plan = db.query
.from("posts")
.match((f) => f.published.eq(false))
.group((f) => ({ _id: f.authorId, draftCount: acc.count() }))
.build();

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

### Join collections with $lookup

Use `.lookup(...)` for a type-checked join against another collection. The joined documents arrive under the name you give `.as(...)`:

```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);
// Each post carries an "author" array with the matching user documents
```

## Choose the right query API

| You need | Use |
| --- | --- |
| CRUD, filters, relations, simple aggregates | ORM API (`db.orm`) |
| Explicit join, computed projection, grouped top-N, `RETURNING` | SQL query 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)`.

## Prompt your coding agent

Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent; the `prisma-next-queries` skill covers both builders and the choice between them and the ORM API. Prompts that map to each section:

- "Using the prisma-next-queries skill, write a SQL builder plan for the top 10 authors by post count."
- "This report needs post and author columns in one flat result. Build the join with the SQL query builder."
- "On MongoDB, group posts per author with the pipeline builder and sort by the count."
- "Review this file and tell me which queries should stay on the ORM API and which need a builder."

## Next

- [Read data](/orm/next/fundamentals/reading-data): the ORM happy path these builders back up.
- [Understand relationships](/orm/next/fundamentals/relations-and-joins) before reaching for explicit joins.
- [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"
]
}
Loading
Loading