From a04714797b911678e8aa837d8c5e8b63240ff6d1 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:26:31 +0200 Subject: [PATCH 1/2] =?UTF-8?q?docs(skills):=20prisma-next-queries=20?= =?UTF-8?q?=E2=80=94=20Postgres=20ORM/SQL=20surface=20is=20always=20namesp?= =?UTF-8?q?ace-qualified?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrections verified against @prisma-next 0.14.0 and current main while writing the Prisma Next Fundamentals docs (prisma/web#8011): - The flat `db.orm.User` / `db.sql.user` form does not exist on Postgres. #778 removed the flat fallback; the orm() proxy resolves only namespace keys, so bare model access returns `undefined` at runtime. All Postgres examples now use `db.orm.public.` / `db.sql.public.`, and the "flat form still works for single-namespace contracts" claim is replaced with the tested behavior. - `.count()` is not a collection terminal (it throws "count() is only available inside include() refinement callbacks"). Counting goes through `.aggregate((a) => ({ n: a.count() }))`; the terminal list is corrected. - SQL-builder inserts must pass rows as an array. The single-object `.insert({...})` form fails at result decoding on 0.14.0; the example now uses `.insert([{...}])` and notes the constraint. Co-Authored-By: Claude Fable 5 --- skills/prisma-next-queries/SKILL.md | 34 +++++------ skills/prisma-next-queries/postgres.md | 81 +++++++++++++------------- 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/skills/prisma-next-queries/SKILL.md b/skills/prisma-next-queries/SKILL.md index 5e8058383f..6160c37d19 100644 --- a/skills/prisma-next-queries/SKILL.md +++ b/skills/prisma-next-queries/SKILL.md @@ -2,7 +2,7 @@ name: prisma-next-queries description: >- Write Prisma Next queries for Postgres, SQLite, or Mongo — pick a lane - (Postgres/SQLite `db.orm.` + `db.sql.
`; Mongo `db.orm.` + (Postgres `db.orm..` + `db.sql..
`; Mongo `db.orm.` + `db.query.from(...)` pipeline builder), filter / project / sort / paginate, eager-load with `.include(...)`, Postgres/SQLite `db.transaction(...)`, Postgres/SQLite ORM `.aggregate(...)`, Mongo aggregations via query builder, @@ -44,21 +44,21 @@ Prisma Next ships **two query lanes per target** on the same `db` value from `sr | Runtime import in `db.ts` | Load | |---|---| -| `@prisma-next/postgres/runtime` | [`postgres.md`](./postgres.md) — `db.orm.` + `db.sql.
` | +| `@prisma-next/postgres/runtime` | [`postgres.md`](./postgres.md) — `db.orm..` + `db.sql..
` | | `@prisma-next/mongo/runtime` | [`mongo.md`](./mongo.md) — `db.orm.` + `db.query.from(...)` | Both targets share the contract and connection on one `db` value. Reach for the ORM first; drop to the lower-level lane when the ORM can't express the shape. Lane choice is local — one query function picks one lane, not the whole app. -**Do not mix target examples.** Postgres uses PascalCase model roots (`db.orm.User`) and `db.sql.user`; Mongo uses lowercased plural roots (`db.orm.users`) and `db.query.from('users')`. There is no `db.sql` on Mongo and no `db.query` SQL-builder equivalent on Postgres. +**Do not mix target examples.** Postgres uses namespace-qualified PascalCase model roots (`db.orm.public.User`) and `db.sql.public.user`; Mongo uses lowercased plural roots (`db.orm.users`) and `db.query.from('users')`. There is no `db.sql` on Mongo and no `db.query` SQL-builder equivalent on Postgres. ## Namespace-aware accessors -When a contract declares more than one namespace (e.g. `public` and `auth`), models and tables are addressed by namespace coordinate: +On Postgres, models and tables are **always** addressed by namespace coordinate (since #778 removed the flat fallback): - **ORM**: `db.orm..` — e.g. `db.orm.public.User`, `db.orm.auth.User` - **SQL builder**: `db.sql..
` — e.g. `db.sql.public.users`, `db.sql.auth.users` -The flat `db.orm.User` / `db.sql.users` form still works for single-namespace contracts (or when all table names are unique across namespaces). When the same bare name appears in more than one namespace, you must use the namespace coordinate. +The flat `db.orm.User` / `db.sql.users` form does **not** work on Postgres, even for single-namespace contracts: the ORM proxy resolves only namespace keys, so `db.orm.User` is `undefined` at runtime (verified against 0.14.0 and current `main`). Single-namespace targets (SQLite, Mongo) keep flat access. See [`postgres.md` § Namespace-aware accessors](./postgres.md#namespace-aware-accessors) for a worked example. @@ -67,7 +67,7 @@ See [`postgres.md` § Namespace-aware accessors](./postgres.md#namespace-aware-a Critical to get right early — on **both Postgres and Mongo**, `.all()` returns an **`AsyncIterableResult`**, which is *both* a `PromiseLike` and an `AsyncIterable`. 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(); +const users = await db.orm.public.User.select('id', 'email').all(); // ^? Row[] ← the Thenable resolves to a real array. This is the default idiom. ``` @@ -76,12 +76,12 @@ You do **not** need a `collect()` / `toArray()` helper — `await` is enough. In ```typescript // Explicit buffering — same outcome as `await ... .all()`, useful when you // want a named Promise to thread through downstream code. -const rows: Promise = db.orm.User.select('id', 'email').all().toArray(); +const rows: Promise = db.orm.public.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()) { +for await (const user of db.orm.public.User.select('id', 'email').all()) { process(user); } ``` @@ -89,9 +89,9 @@ for await (const user of db.orm.User.select('id', 'email').all()) { Two single-row shortcuts also exist on the result, in addition to the collection-level `.first()` (which issues `LIMIT 1` on Postgres): ```typescript -const user = await db.orm.User.where({ id }).all().first(); +const user = await db.orm.public.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(); +const required = await db.orm.public.User.where({ id }).all().firstOrThrow(); // ^? Row ← buffers; throws `RUNTIME.NO_ROWS` if empty. ``` @@ -101,12 +101,12 @@ For genuine single-row reads, prefer the *collection*-level `.first()` (which ad ```typescript // Bad — second await throws RUNTIME.ITERATOR_CONSUMED. -const result = db.orm.User.select('id', 'email').all(); +const result = db.orm.public.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 users = await db.orm.public.User.select('id', 'email').all(); const a = users; const b = users; ``` @@ -121,9 +121,9 @@ When the user is running a one-off `tsx my-script.ts` (not a long-lived server), // src/scripts/seed.ts import { db } from '../prisma/db'; -// Postgres — PascalCase model root from contract +// Postgres — namespace-qualified PascalCase model root from contract for (const u of users) { - await db.orm.User.create(u); + await db.orm.public.User.create(u); } // Mongo — lowercased plural root from contract (e.g. users, not User) @@ -145,10 +145,10 @@ Target-specific pitfalls live in the per-target guides. ## What Prisma Next doesn't do yet -- **N:M `.include()` across a junction table.** The contract IR supports many-to-many relations with a `through` junction table, and `N:M` relations appear as valid relation names on the ORM collection. However, `.include()` on an N:M relation does not emit the two-step junction join — the query plan builder only handles the direct join columns (`localColumn` / `targetColumn`) and ignores the `through` metadata. Attempting it either produces wrong results or an error. Workaround: express the N:M traversal through `db.sql.
` with an explicit join on the junction table. +- **N:M `.include()` across a junction table.** The contract IR supports many-to-many relations with a `through` junction table, and `N:M` relations appear as valid relation names on the ORM collection. However, `.include()` on an N:M relation does not emit the two-step junction join — the query plan builder only handles the direct join columns (`localColumn` / `targetColumn`) and ignores the `through` metadata. Attempting it either produces wrong results or an error. Workaround: express the N:M traversal through `db.sql..
` with an explicit join on the junction table. - **N:M nested mutations.** `mutation-executor.ts` explicitly throws `'N:M nested mutations are not supported yet'` for nested creates/links through an N:M relation. - **`and` / `or` / `not` combinators in the postgres façade.** The combinators currently import from `@prisma-next/sql-orm-client` (an internal package). 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 (Postgres).** `db.orm..groupBy(...).aggregate(...)` materializes a `Promise>` 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.
` 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. +- **`.orderBy(...)` / `.take(...)` on grouped aggregates (Postgres).** `db.orm..groupBy(...).aggregate(...)` materializes a `Promise>` 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..
` 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`. @@ -169,7 +169,7 @@ This skill is split for selective loading. Target-specific reference paths live ## Checklist - [ ] Confirmed the active target from `db.ts` and loaded the matching guide ([`postgres.md`](./postgres.md) or [`mongo.md`](./mongo.md)). -- [ ] For multi-namespace contracts, used `db.orm..` / `db.sql..
` coordinates when the same bare name exists in more than one namespace. +- [ ] On Postgres, used namespace-qualified `db.orm..` / `db.sql..
` coordinates everywhere (the flat form resolves to `undefined`). - [ ] Chose the right lane (ORM by default; lower-level builder for shapes the ORM doesn't express). - [ ] Used `.first()` / `.first({ pk })` (Postgres) or `.where({ ... }).first()` (Mongo) 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. diff --git a/skills/prisma-next-queries/postgres.md b/skills/prisma-next-queries/postgres.md index c7e00c9f88..2b67efdc84 100644 --- a/skills/prisma-next-queries/postgres.md +++ b/skills/prisma-next-queries/postgres.md @@ -8,8 +8,8 @@ Shared concepts (result consumption, script teardown, cross-target pitfalls, cap **Postgres** (`postgres(...)` from `@prisma-next/postgres/runtime`): -- **`db.orm.`** — ORM, PascalCase model name (`db.orm.User`). Fluent `.where(...).select(...).orderBy(...).all()`, fully typed against `Contract`. Default lane for CRUD with relations. -- **`db.sql.
`** — SQL builder, lowercase storage name (`db.sql.user`). Produces a *plan* executed via `db.runtime().execute(plan)`. Use when the ORM is too high-level — explicit `JOIN`, computed projections, set operations, window functions. +- **`db.orm..`** — ORM, namespace-qualified PascalCase model name (`db.orm.public.User`). Fluent `.where(...).select(...).orderBy(...).all()`, fully typed against `Contract`. Default lane for CRUD with relations. +- **`db.sql..
`** — SQL builder, namespace-qualified lowercase storage name (`db.sql.public.user`). Produces a *plan* executed via `db.runtime().execute(plan)`. Use when the ORM is too high-level — explicit `JOIN`, computed projections, set operations, window functions. Reach for the ORM first; drop to `db.sql` when the ORM can't express the shape. Lane choice is local — one query function picks one lane, not the whole app. @@ -17,7 +17,7 @@ Reach for the ORM first; drop to `db.sql` when the ORM can't express the shape. | Need | Choose | Why | |---|---|---| -| Standard CRUD with relations | **ORM (`db.orm.`)** | Highest ergonomics; fully typed; model-shaped. | +| Standard CRUD with relations | **ORM (`db.orm..`)** | Highest ergonomics; fully typed; model-shaped. | | Eager-load related records | **ORM `.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..insert(...).returning(...)`** | ORM returns inserted/updated rows; SQL builder exposes `.returning(...)` explicitly. | @@ -27,23 +27,23 @@ Reach for the ORM first; drop to `db.sql` when the ORM can't express the shape. ## Workflow — ORM reads -The concept: `db.orm.` 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.(value)`. +The concept: `db.orm..` returns a *collection* you compose method-by-method. Each call returns a new collection (immutable chaining); the terminal verb (`.all()` / `.first()` / `.aggregate(...)`) issues the query. Predicates are lambdas over a field proxy: `u.field.(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 }); +const user = await db.orm.public.User.first({ id: userId }); // Returns the full row or `null`. // Find one matching a predicate. -const alice = await db.orm.User +const alice = await db.orm.public.User .where((u) => u.email.eq('alice@example.com')) .first(); // Find many with projection, sort, and limit. -const recentUsers = await db.orm.User +const recentUsers = await db.orm.public.User .select('id', 'email', 'createdAt') .orderBy((u) => u.createdAt.desc()) .take(10) @@ -54,10 +54,10 @@ const recentUsers = await db.orm.User ```typescript // Lambda form — full expression power. -db.orm.User.where((u) => u.email.eq('alice@example.com')); +db.orm.public.User.where((u) => u.email.eq('alice@example.com')); // Shorthand object form — equality on the named fields. -db.orm.User.where({ kind: 'admin' }); +db.orm.public.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(...)` / …). @@ -66,14 +66,14 @@ Operators on the field proxy include `.eq`, `.neq`, `.lt`, `.lte`, `.gt`, `.gte` ```typescript // Chained .where() — each clause AND-composes with the previous one. -await db.orm.Sale +await db.orm.public.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* in SKILL.md -await db.orm.Sale +await db.orm.public.Sale .where((s) => and(s.day.gte(start), s.day.lte(end))) .all(); ``` @@ -85,7 +85,7 @@ The two forms emit the same SQL. Pick chained `.where()` when each clause adds a ```typescript import { and, or, not } from '@prisma-next/sql-orm-client'; -await db.orm.User +await db.orm.public.User .where((u) => and( or(u.kind.eq('admin'), u.email.ilike('%@example.com')), @@ -98,7 +98,7 @@ await db.orm.User **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 +await db.orm.public.Post .where((p) => p.authorId.eq(userId)) .orderBy([(p) => p.createdAt.desc(), (p) => p.id.desc()]) .take(20) @@ -108,13 +108,13 @@ await db.orm.Post **Cursor pagination.** Call `.cursor({ field: lastValue })` after `.orderBy(...)` to resume from a known position. The cursor requires a prior `orderBy` — the type system enforces this. Direction (forward or backward) follows the sort: ascending order means "greater than the cursor value", descending means "less than". ```typescript -const page1 = await db.orm.Post +const page1 = await db.orm.public.Post .orderBy((p) => p.createdAt.desc()) .take(20) .all(); const last = page1[page1.length - 1]!; -const page2 = await db.orm.Post +const page2 = await db.orm.public.Post .orderBy((p) => p.createdAt.desc()) .cursor({ createdAt: last.createdAt }) .take(20) @@ -130,7 +130,7 @@ Cursor keys must match fields in the active `orderBy`. For a composite `orderBy` The concept: `.include('', (branch) => branch.)` 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 +await db.orm.public.User .select('id', 'email') .include('posts', (post) => post @@ -149,27 +149,27 @@ Nested `1:N → 1:N` includes (e.g. `User → posts → comments`) require the c ```typescript // Create — returns the inserted row. -const user = await db.orm.User.create({ id, email, displayName, kind, createdAt }); +const user = await db.orm.public.User.create({ id, email, displayName, kind, createdAt }); // Create with selected return — narrows the return shape. -const summary = await db.orm.User +const summary = await db.orm.public.User .select('id', 'email', 'kind') .create({ id, email, displayName, kind, createdAt }); // Update by predicate. -await db.orm.User.where({ id }).update({ email: newEmail }); +await db.orm.public.User.where({ id }).update({ email: newEmail }); // Update with selected return. -await db.orm.User +await db.orm.public.User .where({ id }) .select('id', 'email', 'kind') .update({ email: newEmail }); // Delete by predicate. -await db.orm.User.where({ id }).delete(); +await db.orm.public.User.where({ id }).delete(); // Upsert — typed by the create branch's shape. -await db.orm.User +await db.orm.public.User .select('id', 'email', 'kind', 'createdAt') .upsert({ create: { id, email, displayName, kind, createdAt: new Date() }, @@ -182,18 +182,18 @@ The ORM returns inserted / updated rows by default. The `.returning(...)` select ## Workflow — Aggregates ```typescript -const totals = await db.orm.User.aggregate((aggregate) => ({ +const totals = await db.orm.public.User.aggregate((aggregate) => ({ totalUsers: aggregate.count(), })); -const adminTotals = await db.orm.User +const adminTotals = await db.orm.public.User .where({ kind: 'admin' }) .aggregate((aggregate) => ({ adminUsers: aggregate.count(), })); // Group-by + aggregate. -const byKind = await db.orm.User +const byKind = await db.orm.public.User .groupBy('kind') .having((having) => having.count().gte(minUsers)) .aggregate((aggregate) => ({ @@ -216,7 +216,7 @@ const byKind = await db.orm.User 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 +const revenue = await db.orm.public.Sale .where((s) => s.day.gte(start)) .aggregate((a) => ({ total: a.sum('amount') })); // revenue.total: number | null @@ -226,16 +226,16 @@ const safe = revenue.total ?? 0; // ← apply at the consumption site, not in 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. -## Workflow — SQL builder (`db.sql.
`) +## Workflow — SQL builder (`db.sql..
`) -The concept: `db.sql.
` 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. +The concept: `db.sql..
` 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 +const plan = db.sql.public.post .select('id', 'title', 'userId', 'createdAt') .where((f, fns) => fns.eq(f.userId, userId)) .limit(limit) @@ -249,15 +249,16 @@ The `.where(...)` callback receives `(fields, fns)` — `fields` is the field pr ### `INSERT` / `UPDATE` / `DELETE` with `RETURNING` ```typescript -// Insert and return selected columns. -const plan = db.sql.user - .insert({ email }) +// Insert and return selected columns. Pass rows as an array — the +// single-object form currently fails at result decoding (verified on 0.14.0). +const plan = db.sql.public.user + .insert([{ email }]) .returning('id', 'email') .build(); const [row] = await db.runtime().execute(plan); // Update with predicate and returning. -const updatePlan = db.sql.user +const updatePlan = db.sql.public.user .update({ email: newEmail }) .where((f, fns) => fns.eq(f.id, userId)) .returning('id', 'email') @@ -265,7 +266,7 @@ const updatePlan = db.sql.user const rows = await db.runtime().execute(updatePlan); // Delete with predicate. -const deletePlan = db.sql.user +const deletePlan = db.sql.public.user .delete() .where((f, fns) => fns.eq(f.id, userId)) .build(); @@ -278,7 +279,7 @@ await db.runtime().execute(deletePlan); ```typescript // Project a computed expression alongside model fields. -const plan = db.sql.cafe +const plan = db.sql.public.cafe .select('id', 'name') .select('meters', (f, fns) => fns.distanceSphere(f.location, point)) .orderBy((f, fns) => fns.distanceSphere(f.location, point), { direction: 'asc' }) @@ -288,8 +289,8 @@ const plan = db.sql.cafe 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)) +db.sql.public.post + .innerJoin(db.sql.public.post.as('p2'), (f, fns) => fns.ne(f.p1.userId, f.p2.userId)) // ... .build(); ``` @@ -330,7 +331,7 @@ const user = await db.orm.public.User.create({ id: 1, email: 'a@x.io' }); const authUser = await db.orm.auth.User.create({ id: 2, token: 'tok' }); ``` -The flat `db.sql.users` / `db.orm.User` form still works when bare names are unique across all namespaces. When the same bare name appears in more than one namespace, use the coordinate form — both the type system and the runtime require it to resolve to the right table. +There is no flat `db.sql.users` / `db.orm.User` form: the namespace coordinate is always required on Postgres. The runtime proxy resolves only namespace keys, so a bare model access returns `undefined` (verified against 0.14.0 and current `main`; the flat fallback was removed in #778). Cross-namespace relations (e.g. `public.Profile` → `auth.User`) follow the same `.include()` syntax; the ORM resolves the correct schema-qualified join automatically. @@ -346,7 +347,7 @@ Cross-namespace relations (e.g. `public.Profile` → `auth.User`) follow the sam 8. **Setting `capabilities: { lateral: 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`). 9. **Confabulating a `db.sql.raw(...)`, TypedSQL, or `.stream()` surface.** None of those exist today. See *What Prisma Next doesn't do yet* in [`SKILL.md`](./SKILL.md). 10. **Mixing the ORM mutation return with `runtime.execute(plan)`.** ORM terminals issue the query themselves and return rows. `runtime.execute` is for SQL-builder plans. -11. **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(...)`. Fine at small cardinalities; for large grouped result sets, drop to `db.sql.
`. +11. **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(...)`. Fine at small cardinalities; for large grouped result sets, drop to `db.sql..
`. ## Reference Files @@ -364,5 +365,5 @@ Cross-namespace relations (e.g. `public.Profile` → `auth.User`) follow the sam - [ ] 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. -- [ ] For top-N grouped aggregates at meaningful scale, dropped to `db.sql.
` rather than JS-side sort + slice over `groupBy(...).aggregate(...)`. +- [ ] For top-N grouped aggregates at meaningful scale, dropped to `db.sql..
` rather than JS-side sort + slice over `groupBy(...).aggregate(...)`. - [ ] 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. From c9cf152cd560092a0e5e1a16da0fdc065d67a9fe Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:56:34 +0200 Subject: [PATCH 2/2] docs(skills): address review on prisma-next-queries - Explain where the namespace coordinate comes from: top-level PSL models live in the default public namespace, models inside a namespace { } block are addressed by that namespace (SevInf). - Drop the historical note about the flat fallback's removal; state the rule plainly (SevInf). - Guard the empty first page before deriving the cursor in the pagination example (CodeRabbit). - Promise -> Promise for consistency with the surrounding Row[] text (CodeRabbit). - Make the short-script seed example self-contained by defining the users array it iterates (CodeRabbit). Co-Authored-By: Claude Fable 5 --- skills/prisma-next-queries/SKILL.md | 11 ++++++++--- skills/prisma-next-queries/postgres.md | 18 +++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/skills/prisma-next-queries/SKILL.md b/skills/prisma-next-queries/SKILL.md index 6160c37d19..10b147dca7 100644 --- a/skills/prisma-next-queries/SKILL.md +++ b/skills/prisma-next-queries/SKILL.md @@ -53,12 +53,12 @@ Both targets share the contract and connection on one `db` value. Reach for the ## Namespace-aware accessors -On Postgres, models and tables are **always** addressed by namespace coordinate (since #778 removed the flat fallback): +On Postgres, models and tables are **always** addressed by namespace coordinate: - **ORM**: `db.orm..` — e.g. `db.orm.public.User`, `db.orm.auth.User` - **SQL builder**: `db.sql..
` — e.g. `db.sql.public.users`, `db.sql.auth.users` -The flat `db.orm.User` / `db.sql.users` form does **not** work on Postgres, even for single-namespace contracts: the ORM proxy resolves only namespace keys, so `db.orm.User` is `undefined` at runtime (verified against 0.14.0 and current `main`). Single-namespace targets (SQLite, Mongo) keep flat access. +The flat `db.orm.User` / `db.sql.users` form does **not** work on Postgres, even for single-namespace contracts: `db.orm.User` is `undefined` at runtime. Models declared at the top level of the PSL live in the default `public` namespace; models declared inside a `namespace { ... }` block use that namespace. Single-namespace targets (SQLite, Mongo) keep flat access. See [`postgres.md` § Namespace-aware accessors](./postgres.md#namespace-aware-accessors) for a worked example. @@ -76,7 +76,7 @@ You do **not** need a `collect()` / `toArray()` helper — `await` is enough. In ```typescript // Explicit buffering — same outcome as `await ... .all()`, useful when you // want a named Promise to thread through downstream code. -const rows: Promise = db.orm.public.User.select('id', 'email').all().toArray(); +const rows: Promise = db.orm.public.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 @@ -121,6 +121,11 @@ When the user is running a one-off `tsx my-script.ts` (not a long-lived server), // src/scripts/seed.ts import { db } from '../prisma/db'; +const users = [ + { email: 'alice@example.com', displayName: 'Alice' }, + { email: 'bob@example.com', displayName: 'Bob' }, +]; + // Postgres — namespace-qualified PascalCase model root from contract for (const u of users) { await db.orm.public.User.create(u); diff --git a/skills/prisma-next-queries/postgres.md b/skills/prisma-next-queries/postgres.md index 2b67efdc84..2e202bc212 100644 --- a/skills/prisma-next-queries/postgres.md +++ b/skills/prisma-next-queries/postgres.md @@ -11,6 +11,8 @@ Shared concepts (result consumption, script teardown, cross-target pitfalls, cap - **`db.orm..`** — ORM, namespace-qualified PascalCase model name (`db.orm.public.User`). Fluent `.where(...).select(...).orderBy(...).all()`, fully typed against `Contract`. Default lane for CRUD with relations. - **`db.sql..
`** — SQL builder, namespace-qualified lowercase storage name (`db.sql.public.user`). Produces a *plan* executed via `db.runtime().execute(plan)`. Use when the ORM is too high-level — explicit `JOIN`, computed projections, set operations, window functions. +The namespace coordinate comes from your PSL: models declared at the top level of the schema live in the default `public` namespace, and models declared inside a `namespace { ... }` block are addressed by that namespace (`db.orm..`, `db.sql..
`). The examples below use `public`; substitute your own namespace where a model is declared in one. + Reach for the ORM first; drop to `db.sql` when the ORM can't express the shape. Lane choice is local — one query function picks one lane, not the whole app. **Lane decision table:** @@ -113,12 +115,14 @@ const page1 = await db.orm.public.Post .take(20) .all(); -const last = page1[page1.length - 1]!; -const page2 = await db.orm.public.Post - .orderBy((p) => p.createdAt.desc()) - .cursor({ createdAt: last.createdAt }) - .take(20) - .all(); +const last = page1[page1.length - 1]; +const page2 = last + ? await db.orm.public.Post + .orderBy((p) => p.createdAt.desc()) + .cursor({ createdAt: last.createdAt }) + .take(20) + .all() + : []; ``` Cursor keys must match fields in the active `orderBy`. For a composite `orderBy`, pass a value for each ordering column — a partial cursor seeks only on the columns you supply, which gives an incomplete keyset. An empty cursor object is a no-op: you get the unfiltered first page back. @@ -331,7 +335,7 @@ const user = await db.orm.public.User.create({ id: 1, email: 'a@x.io' }); const authUser = await db.orm.auth.User.create({ id: 2, token: 'tok' }); ``` -There is no flat `db.sql.users` / `db.orm.User` form: the namespace coordinate is always required on Postgres. The runtime proxy resolves only namespace keys, so a bare model access returns `undefined` (verified against 0.14.0 and current `main`; the flat fallback was removed in #778). +There is no flat `db.sql.users` / `db.orm.User` form: the namespace coordinate is always required on Postgres. A bare model access returns `undefined`, so the first chained call throws. Cross-namespace relations (e.g. `public.Profile` → `auth.User`) follow the same `.include()` syntax; the ORM resolves the correct schema-qualified join automatically.