TML-2729: drop LATERAL include codegen for correlated-only read path#667
Conversation
|
Warning Review limit reached
More reviews will be available in 18 minutes and 23 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis PR removes the lateral-vs-correlated include strategy model and enforces correlated-subquery-only lowering across the SQL ORM. It deletes the includeMany feature documentation, strategy selection infrastructure, and lateral join emission code, while rewriting query planners, dispatchers, and test coverage to validate correlated-subquery behavior exclusively. ChangesCorrelated Subquery Includes
🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
size-limit report 📦
|
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/emitter
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
…-only read path The read-path include dispatch carried two single-query builders — buildLateralIncludeArtifacts (LEFT JOIN LATERAL + json_agg) for Postgres and buildCorrelatedIncludeProjection (correlated subquery) for SQLite — selected on the `lateral` capability flag. Benchmarking on PG 17.5/17.10 proved the two forms compile to structurally identical plans for the top-N-per-parent shape (the per-parent LIMIT forbids de-correlation of either form), so LATERAL offers no planner advantage for includes. Route every include through the correlated path and remove the strategy axis entirely (TML-2657 already removed the multi-query fallback, so the dispatch was binary): delete buildLateralIncludeArtifacts, drop the `strategy` parameter from every include builder, simplify buildNestedIncludeArtifacts to projections-only (correlated emits no joins), rename compileSelectWithIncludeStrategy to compileSelectWithIncludes, and delete include-strategy.ts (selectIncludeStrategy / IncludeStrategy). The `lateral` capability flag, JoinAst.lateral, the renderer LATERAL emission, and the public lateralJoin() DSL are independent consumers and are untouched. A new regression guard pins that a lateral-capable contract now resolves includes in a single execution with no LATERAL join and no LATERAL keyword in the lowered SQL. Refs: TML-2729 Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…s with correlated lowering `includeMany` is no longer a method on any surface — the ORM exposes `.include(...)` and the SQL builder has no include method. Clean up the references this leaves behind, and align the adapter docs with the correlated-only include lowering this branch introduces: - Delete the obsolete `include-many-patterns.mdc` rulecard (it documented a removed `SelectBuilderImpl.includeMany()` API) and its index entry. - Remove the dead `HasIncludeManyCapabilities` type (zero usages; it encoded the now-defunct lateral+jsonAgg include gate). - Postgres README: includes now lower to a correlated subquery, not `LEFT JOIN LATERAL`; note LATERAL emission is retained only for the public `lateralJoin()` DSL. - SQLite README: rename includeMany -> include; correlated is the strategy, not a fallback. - AGENTS.md (capability-gating example), the queries skill gotcha, and a stale e2e describe label: includeMany -> include / a real capability. - Remove the broken `### Queries with includeMany` SQL-builder example from query-patterns.md (it called a nonexistent `.includeMany()`). ADRs and the Query Lanes subsystem doc are left as-is (historical / architecture docs handled separately). Refs: TML-2729 Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
… nested-include helper - Rename `buildNestedIncludeArtifacts` -> `buildNestedIncludeProjections` and return the projection array directly instead of wrapping it in an object. - Postgres/SQLite READMEs: describe what the adapter actually renders at the AST level (JSON aggregation functions, scalar subqueries, LATERAL joins) and drop all references to `.include(...)`, lanes, and the ORM client — the adapter does not know which lane produced the AST. - include.test.ts: drop transient ticket IDs from test names/comments. - nested-includes-strategy.test.ts: assert `ast.joins` is exactly `[]` (strictly stronger than "no lateral join") in the lateral-flag-inert guard. Refs: TML-2729 Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
72db66a to
291bb99
Compare
…risma#679) Slice 1 of the **SQL ORM: Many-to-Many End to End** project ([Linear project](https://linear.app/prisma-company/project/sql-orm-many-to-many-end-to-end-c178df40ca3a)). Reads an M:N relation through its junction. > **Stacked PR.** Base is `tml-2784` (prisma#678, slice 0) → `tml-2597` (prisma#673) → `tml-2729` (prisma#667) → `main`. Review/merge bottom-up. ## Overview `db.orm.User.include(tags)` now resolves a many-to-many relation to `{ …user, tags: Tag[] }` in a **single correlated subquery** that walks parent → junction → target — no `LATERAL`, no second query. Built on slice 0s `ResolvedRelation.through`. ## Changes (4 commits) - **`fcecac5b3`** — integration fixture gains a `User ↔ Tag` M:N relation via a `UserTag` junction (composite PK `user_id`/`tag_id`); `contract.json`/`.d.ts` re-emitted. - **`e587b433c`** — read path: `IncludeExpr.through` (surfaced by `resolveIncludeRelation`), and `buildCorrelatedIncludeProjection` gains an M:N branch — `buildManyToManyJunctionArtifacts` builds a non-LATERAL inner join to the junction (`junction.childColumns = target.targetColumns`) correlated to the parent (`junction.parentColumns = parent` anchor), composite-key AND-ed; FK decode path reused. Unit-tested at the AST level. - **`b9c3e9f7b`** — replace 2 bare `as` casts with `castAs`; add the missing M:N + distinct + non-leaf unit test. - **`d3232cbad`** — 7 integration tests (PGlite). ## Integration tests (per the project standard) Whole-row `toEqual`; 6/7 use explicit `.select(...)` (so adding a model field wont churn assertions); **test 5 uses implicit/default selection** (full `User` + `tags: Tag[]` shape); a **single-execution / no-`LATERAL`** assertion; depth-2 nesting (`invitedUsers → tags`); edges (user with no tags → `tags: []`; a tag shared by multiple users). ## Why This is the first of the three relation-shaped M:N consumers (read / filter / write) over slice 0s shared `through` primitive. The correlated-only approach matches the post-TML-2729 read path (no LATERAL to reintroduce). ## Scope / notes Read only — filter (TML-2786) and write (TML-2787) are later slices. The fixture is **one-directional** (`User.tags`; reverse `Tag.users` deferred — adding it trips a latent create-overload type fragility in unrelated mutation-defaults tests; see the projects unattended-decisions log). Fixture re-emit used a `tsx` bypass because the CLI `contract emit` fails on a sandbox config-load env issue — **CI `fixtures:check` is the real golden-stability gate**; please confirm its green (or re-run the canonical emit). Broad integration runs show pre-existing PGlite/WASM JIT flakiness; the M:N tests pass on targeted runs. Refs: TML-2785. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * include(...) now supports many-to-many (M:N) relationships via junction tables, returning correct nested arrays and preserving single-query execution. * **Tests** * Added unit and integration tests covering M:N includes, composite keys, nested includes, distinct+nested scenarios, and end-to-end result shape correctness. * **Documentation** * Added upgrade notes for 0.14 describing the runtime support for M:N correlated include reads. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…e junction (prisma#680) Slice 2 of the **SQL ORM: Many-to-Many End to End** project ([Linear project](https://linear.app/prisma-company/project/sql-orm-many-to-many-end-to-end-c178df40ca3a)). Filters an M:N relation through its junction. > **Stacked PR.** Base is `tml-2785` (prisma#679, slice 1) → `tml-2784` (prisma#678) → `tml-2597` (prisma#673) → `tml-2729` (prisma#667) → `main`. Review/merge bottom-up. ## Overview `db.orm.User.filter((u) => u.tags.some/every/none((t) => …))` now emits an EXISTS / NOT EXISTS subquery that walks the `UserTag` junction. Previously `buildJoinWhere` read only `relation.on.localFields/targetFields`, so an M:N filter produced a wrong-shape EXISTS that skipped the junction. ## Changes (2 commits) - **`f9226ccb9`** — `model-accessor.ts`: `buildManyToManyExistsExpr` builds the two-sided junction correlation — junction→target (`through.childColumns = target.targetColumns`) and junction→parent (`through.parentColumns = parent.{on.localFields resolved}`, mirroring slice 1s read correlation), composite-key AND-ed. Shapes: `some`=`EXISTS`, `none`=`NOT EXISTS`, `every`=`NOT EXISTS(… AND NOT(pred))`, vacuous `every({})`=`AndExpr.true()` (consistent with the FK `every` path). Dispatch via a `hasThrough` **type predicate** (no bare cast). 6 AST unit tests. - **`e65a9db43`** — 9 integration tests (PGlite). ## Integration tests (per the project standard) Whole-row `toEqual` on the **filtered user set**; 8/9 use explicit `.select`; **test 6 uses implicit/default selection**. Covers `some`, `none`, `every` (incl. the vacuous tag-less-user-qualifies case, tested in isolation and alongside partial-match exclusion), and empty-match edges for all three operators. ## Why Second of the three relation-shaped M:N consumers over slice 0s shared `through` primitive. The filter correlation mirrors slice 1s read correlation for consistency. ## Scope / notes Filter only — write (TML-2787) is the last slice. No fixture change (reuses slice 1s `User ↔ Tag` + its seed helpers). No production change in the test dispatch. Broad integration runs show pre-existing PGlite/WASM JIT flakiness (logged); the M:N filter tests pass on targeted runs. Refs: TML-2786. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Many-to-many relation filters now supported: use tags.some(...), tags.none(...), and tags.every(...) on M:N relations with correct EXISTS/NOT EXISTS semantics. * **Tests** * Added unit and integration tests covering M:N filters, composite-key and self-referential joins, vacuous-every behavior, and error cases for invalid junction metadata. * **Documentation** * Upgrade notes updated to mention M:N relation filter support. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Slice 3 (final) of the **SQL ORM: Many-to-Many End to End** project ([Linear project](https://linear.app/prisma-company/project/sql-orm-many-to-many-end-to-end-c178df40ca3a)). Nested `connect`/`disconnect`/`create` through the junction + the required-payload safety rail. > **Stacked PR.** Base `tml-2786` (prisma#680) → `tml-2785` (prisma#679) → `tml-2784` (prisma#678) → `tml-2597` (prisma#673) → `tml-2729` (prisma#667) → `main`. Review/merge bottom-up. ## Overview `db.orm.User.update/create({ tags: (t) => t.connect/disconnect/create(...) })` now routes to the `UserTag` junction (INSERT / DELETE / target-insert+link), under both `create()` and `update()`. The `N:M not supported yet` guard is gone. Junctions with a **required non-FK payload column** cant be written through the sugar, so `create` **and** `connect` on them are disabled at both the type level and runtime with a clear error (disconnect stays). ## Changes - **Runtime junction write path:** `partitionByOwnership` gains a `junctionOwned` bucket (keyed on `through` presence); connect→INSERT, disconnect→DELETE, create→target-insert+link, both flows, composite-key AND-ed; the rejection unit test flipped positive. (`getRelationDefinitions` now carries `through`.) - **Required-payload fixture:** `User ↔ Role` via `UserRole(user_id, role_id, level NOT NULL)` (canonical CLI emit). - **Runtime guard:** nested `create` **and** `connect` on a required-payload junction throw (connect also INSERTs a junction row it cant complete → DB NOT-NULL violation). Disconnect stays allowed. - **Type-level gate:** `HasRequiredJunctionPayload` + `HasJunctionThrough` compose `LinkWritesDisabled` and `BareDisconnectDisabled` on `RelationMutator`, disabling `create`/`connect` and bare `disconnect()` at the type level for required-payload junctions. The `.d.ts` emitter now carries `through` (namespace-scoped), so the gate derives required-payload columns from the junction model's field types without `any`. Covered by type tests (`junction-link-write-disable.test-d.ts`). - **Error handling:** `isUniqueConstraintViolation` uses driver-normalized `SqlQueryError.sqlState`; duplicate-connect wraps with a domain error naming the junction; shared-column conflicts detected on both INSERT and DELETE sides; metadata-length assertions fail fast; preflight detects duplicate resolved targets. - **Namespace scoping:** `MutationDefaultsOptions` carries `namespace`; `ModelNameForTable` and `JunctionPayloadFieldNames` are namespace-aware; execution-default and storage-default columns are excluded from `requiredPayloadColumns`. - **Transaction safety:** ORM mutations run inside a transaction via `withMutationScope`. ## Integration tests (per the project standard) `mn-nested-write.test.ts` — connect/create/disconnect on the pure `User.tags` junction and the required-payload `User.roles` junction, both `create()` and `update()` flows; duplicate-connect rejection; missing-target rejection; parent-insert-failure-after-connect-preflight (no partial writes); execution-defaulted junction payload column; bare `disconnect()` type error on junctions; whole-row `toEqual`, explicit `.select` in most, ≥1 implicit/default selection. ## AC status - ✅ **AC-1** — connect/disconnect/create route to junction DML, both flows, guard removed, rejection test flipped (unit + integration). - ✅ **AC-2** — Runtime disable of `create`+`connect` on required-payload junctions + type-level disable (`create`/`connect` → `never`, bare `disconnect()` → type error on junctions). Both implemented and tested. - ✅ **AC-3** — write integration tests per the standard. ## Notes `connect`-disabled-on-required-payload was a mid-flight spec correction (the original spec wrongly assumed connect was FK-pair-only safe). Duplicate `connect` errors deliberately rather than silently succeeding (intentional divergence from Prisma-classic implicit-M:N `connect`, which is idempotent via `ON CONFLICT DO NOTHING`). The reverse `Tag.users`/`Role.users` directions are deferred (one-directional fixture, decision prisma#3). Refs: TML-2787. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Enabled many-to-many nested mutations via junction tables when junction metadata is available, including `create`, `connect`, and `disconnect`. * Added compile-time and runtime protections to prevent creating/connecting junction links when the junction has required payload fields. * **Bug Fixes** * Improved unique-constraint error detection for more consistent handling (based on SQLSTATE). * **Tests** * Expanded unit and integration coverage for junction-based M:N nested writes, junction payload validation, and correct junction DML behavior. * **Chores** * Updated generated contract and example artifacts to use namespace-qualified execution-default references. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…–6) (prisma#742) > **Supersedes prisma#697** — that PR was auto-marked *merged* by GitHub when a botched force-push briefly made its head identical to its base (nothing was actually merged; the branch content is unchanged). Same commits, same review state. Follow-on to the [SQL ORM: Many-to-Many End to End](https://linear.app/prisma-company/project/sql-orm-many-to-many-end-to-end-c178df40ca3a) project: M:N **demo examples** + the project-plan expansion they surfaced. > **Stacked PR** — top of the M:N stack: `tml-2787` (prisma#683) → … → `tml-2729` (prisma#667) → `main` (the whole stack is rebased onto the latest `main`). ## SQLite demo M:N examples (`72ef8b793`, `883309ecc`) The SQLite demo (`examples/prisma-next-demo-sqlite`, TS-authored) now demonstrates the full M:N ORM API via a pure `Post ↔ Tag` junction (`PostTag`): - **Read:** `get-post-tags` — `.include(tags, t => t.select(...))`. - **Filter:** `get-posts-by-tag-filter` — `.where(p => p.tags.some/none/every(t => t.label.eq(...)))`. - **Write (callback mutator):** `connect-post-tags` / `disconnect-post-tags` / `create-post-with-tags` — `.update/.create({ tags: t => t.connect/disconnect/create([...]) })` with readback. Wired as 6 CLI commands + seed; **smoke-tested end-to-end** (SQLite is offline-runnable); emitted contract carries `cardinality:N:M` + `through`; `emit:check` + typecheck clean. ## Why only SQLite (and the plan expansion) (`d711adfb6`) Adding examples surfaced a real gap: **the navigable M:N API is authorable only via the TS contract builder (`rel.manyToMany`), not PSL** — PSL emits only `1:N`/`N:1` and routes M:N to explicit junction models. The PG demo emits from PSL, so it **cant** show M:N until PSL learns to author it. So: - **Filed [TML-2794](https://linear.app/prisma-company/issue/TML-2794)** — PSL many-to-many authoring (the framework gap). - **Filed [TML-2795](https://linear.app/prisma-company/issue/TML-2795)** — PG demo M:N examples + pre-existing dual-mode contract drift (blocked by TML-2794). - **Amended the project spec + plan** with follow-on **slices 4–6** (SQLite examples [done], PSL M:N authoring [planned], PG demo [planned]) + slice specs/plans. Slice 5 is framework-scoped and flagged for possible promotion to its own project. ## Scope / notes This PR ships the **SQLite** examples + the planning docs only. PG demo examples + PSL authoring are tracked (TML-2794/2795) and specd but not implemented here. No production `src/` changes — demo + project-docs only. Refs: TML-2790. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Expanded the sqlite demo to support `Post` ↔ `Tag` many-to-many tagging via a junction. * Added CLI commands to query posts/tags, apply tag filters (`some`/`none`/`every`), and connect/disconnect or create posts with tags. * **Documentation** * Updated the demo README to cover the full many-to-many API, command behaviors, and junction insert/delete notes. * Enhanced seeding instructions and the seed process to create tags and junction rows. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Linked issue
Refs TML-2729.
Builds on TML-2657 (already merged), which removed the multi-query include fallback and left the read-path dispatch as a binary
lateral/correlatedchoice. This PR removes the remaining axis.At a glance
The whole include lowering in
compileSelectWithIncludesis now a single unconditional correlated arm — no capability branch, no LATERAL builder:Previously this loop branched on
selectIncludeStrategy(contract): alateral-capable target (Postgres) went throughbuildLateralIncludeArtifacts(LEFT JOIN LATERAL … json_agg), everything else through the correlated subquery. Both compiled to structurally identical query plans, so the branch bought nothing.Decision
Route every SQL ORM include through the correlated-subquery path and remove the lateral-vs-correlated strategy axis entirely. Concretely, this PR:
buildLateralIncludeArtifacts()andinclude-strategy.ts(selectIncludeStrategy+ theIncludeStrategytype).strategy: 'lateral' | 'correlated'parameter from every include builder in the chain, so the correlated path is unconditional.compileSelectWithIncludeStrategy→compileSelectWithIncludes(it no longer selects anything).lateralcapability flag,JoinAst.lateral, the Postgres renderer'sLATERALemission, and the publiclateralJoin()/outerLateralJoin()SQL-builder DSL.The justification is empirical, not aesthetic. On PostgreSQL 17.5 (PGlite) and native 17.10, LATERAL and the correlated subquery produce byte-identical results and structurally identical
EXPLAIN (ANALYZE, BUFFERS)plans for the contested top-N-per-parent shape (include({ take, orderBy })) — same innerAggregate → Limit → Index Scan, same buffers, same loop counts. The per-parentLIMITforbids Postgres from de-correlating either form, so both are forced into the same parameterized per-parent plan; LATERAL offers no planner advantage for includes. Full verbatim plans and a self-contained repro are in the TML-2729 ticket comment. This is a codegen simplification with a hard "must not be slower" bar, which the measurements satisfy with margin.How it fits together
Collapse the dispatch.
collection-dispatch.tsno longer imports or callsselectIncludeStrategy;dispatchWithIncludescalls the renamedcompileSelectWithIncludesdirectly. The stale "lateral / correlated builders pick on the capability flag" comments are reworded to describe the single correlated single-query builder.Unconditional correlated lowering. In
query-plan-select.tsthestrategyparameter is removed frombuildNestedIncludeArtifacts,buildIncludeChildRowsSelect,buildDistinctNonLeafChildRowsSelect,buildIncludeChildCombineSelect,buildIncludeChildCombineBranchSelect, andbuildIncludeChildRowsAggregateSelect.buildNestedIncludeArtifactsis simplified to return projections only (correlated emits no joins), which lets the child SELECT drop its now-always-empty.withJoins(nestedJoins)wiring.Delete the dead surface.
buildLateralIncludeArtifacts,selectIncludeStrategy, theIncludeStrategytype, and their test file are removed; the re-export inquery-plan.tsis renamed. A repo-wide grep for all four symbols (excludingdist) returns zero hits.Re-express the tests against one shape. The dual-strategy unit and integration suites collapse to a single correlated path per scenario (depth-2/3 nesting, self-relations,
combine(), scalar reducers, distinct-non-leaf, pagination), and a new guard pins this PR's intent: alateral-capable contract still resolves includes in one execution with no LATERAL.Behavior changes & evidence
Postgres includes now lower to correlated subqueries instead of
LEFT JOIN LATERAL. Results, ordering,take/skip,distinct,combine(), and scalar reducers are unchanged — only the SQL primitive differs. Implementation:query-plan-select.ts. Evidence:test/sql-orm-client/include.test.tsrewrites its two former lateral-join assertions to assert the correlated-subquery shape (no lateral join, noLATERALkeyword).The
lateralcapability flag is now inert for include codegen. Advertisinglateral: trueno longer changes the emitted include SQL. Evidence: a dedicated guard intest/sql-orm-client/nested-includes-strategy.test.tsasserts alateral-capable contract resolves a depth-2 include in one execution,ast.joinscontains no.lateraljoin, and the lowered SQL contains noLATERALkeyword.Single-execution (no N+1) guarantee preserved at every depth.
compileSelectWithIncludesstill builds one AST → one query plan; nested includes embed as nested correlated subqueries. Evidence: the execution-count assertions (executions.toHaveLength(1)at depth-2, depth-3, and self-relation) innested-includes-strategy.test.ts.Reviewer notes
query-plan-select.test.tsshrinks by ~560 lines: thelateral-specific describe blocks and the duplicated lateral arms of each scenario are gone, with the unique coverage (count-over-where-filtered, drops-orderBy, distinctOn/offset) re-expressed against the correlated emission. Net across the PR is −794 lines. Spot-check that no behavior coverage was lost rather than moved.include.test.tsdeviation. Two of its tests assertedjoin.lateral === truefor an include join — those would now fail at runtime, so I rewrote them to assert the correlated shape. This is slightly beyond a comment-only touch but is required by the change (the lateral flag is now inert for includes).includeJoinsis not dead. After removing the lateral arm fromcompileSelectWithIncludes,includeJoinsis still populated by MTI/polymorphism variant joins and flows into the AST — it's just no longer fed by includes. TheJoinAstimport remains in genuine use (combine branches still useJoinAst.inner).projects/middleware-intercept-and-cache/follow-ups.mdnamed the renamed symbol; updated tocompileSelectWithIncludes.packages/3-extensions/, the coverage gate requires an extension-author upgrade directory for the in-flight transition. Addedskills/extension-author/prisma-next-extension-upgrade/upgrades/0.12-to-0.13/instructions.mdwithchanges: []— none of the touched sql-orm-client symbols are in the package's publicexports/index.ts, so there's no extension-author-facing change. (The branch was also merged up to currentmainat 0.12.0.)includeManyreferences surfaced during review.includeManyis no longer a method on any surface (the ORM exposes.include(...); the SQL builder has no include method). This commit deletes the obsoleteinclude-many-patterns.mdcrulecard, removes the deadHasIncludeManyCapabilitiestype, aligns the Postgres/SQLite adapter READMEs with correlated lowering (Postgres no longer documentsLEFT JOIN LATERALfor includes), fixes a capability-gating example inAGENTS.md, the queries-skill gotcha, and a stale e2edescribelabel, and removes a broken.includeMany()SQL-builder example fromdocs/reference/query-patterns.md. Reviewable independently of the codegen change.Compatibility / migration / risk
Internal only — no public API, SPI, CLI, contract, or error-code surface changes. The four LATERAL consumers the ticket flags as off-limits are verified untouched (the Postgres renderer,
JoinAst.lateral, thelateralcapability flag, and thelateralJoin()DSL are not in the diff). Risk is a SQL-plan regression on Postgres, ruled out by the benchmark in the ticket and the unchanged single-execution guards.Testing performed
Run on final HEAD:
pnpm build— 65/65 packages.pnpm typecheck— 135/135 (includes@prisma-next/integration-tests).pnpm lint:deps— clean (970 modules, no dependency violations).@prisma-next/sql-orm-clientpackage tests — 485 passed, 3 skipped.@prisma-next/integration-teststest/sql-orm-client/— 187 passed (24 files), including the new lateral-flag-inert guard.Two pre-existing, environmental failure sets reproduced and were ruled out as unrelated to this change (both pass / are absent in the touched blast radius): the postgres adapter's
migrations/runner.policy.integration.test.tsthrows an uncaughtConnection terminated(needs a live PG that drops in the sandbox) and pollutes a few CLI test files in the sametest:packagesworker — those CLI files pass in isolation; and thetest/integration/cli-journeys/*.e2e.test.tssuites fail onpnpm install --no-frozen-lockfile(no network in the sandbox).Skill update
n/a — internal only. No user-facing surface (CLI, public TypeScript API, config, error codes) changed.
Follow-ups
take, large-fanout case (aGROUP BYpre-aggregate + join) that neither LATERAL nor correlated emits today; pursuing it would be a separate optimization, not a regression introduced here.docs/reference/query-patterns.mdanddocs/architecture docs/subsystems/3. Query Lanes.mdcarry broader staleness thanincludeMany— they document adb.sql.from(...)/db.schema.tablesSQL-builder surface that no longer exists. Left for a dedicated doc audit rather than rewritten piecemeal here. The three ADRs that mentionincludeManyare left as point-in-time historical records.Alternatives considered
'correlated'but keep thestrategyparameter threaded through. Rejected: TML-2657 already removed the multi-query fallback, leaving a binary that a dead parameter would only obscure. Removing the axis entirely is the honest end-state.GROUP BYpre-aggregate plan for includes. Rejected for this PR: it's incompatible with per-parentLIMIT(no top-N-per-group form) and is emitted by neither the old nor the new path, so it's a separate optimization rather than part of this simplification.Checklist
git commit -s) per the DCO.TML-NNNN: <sentence-case title>form.