-
Notifications
You must be signed in to change notification settings - Fork 974
docs(next): Prisma Next Fundamentals section (DR-8681) #8011
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
e2b2984
docs(next): add Fundamentals section with tested Postgres/MongoDB exa…
ankur-arch 101699f
docs(next): move Fundamentals under /orm/next, fix code tabs, rewrite…
ankur-arch 6d29d03
docs(next): teach relationships with diagrams, add streaming, restruc…
ankur-arch 90a65d3
docs(next): add context, result shapes, and agent prompts to Fundamen…
ankur-arch 7a88355
Merge branch 'main' into pn-fundamentals-DR-8681
ankur-arch d7a66b3
docs(next): address review feedback, show examples for prose-only gui…
ankur-arch 70661c8
docs(next): park cutover redirects as comments, document section conv…
ankur-arch File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
apps/docs/content/docs/(index)/next/fundamentals/advanced-queries.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
10
apps/docs/content/docs/(index)/next/fundamentals/meta.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
174
apps/docs/content/docs/(index)/next/fundamentals/reading-data.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| ``` | ||
|
|
||
| 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. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.