Skip to content

TML-2729: drop LATERAL include codegen for correlated-only read path#667

Merged
aqrln merged 3 commits into
mainfrom
tml-2729-sql-orm-drop-lateral-for-includes-in-favour-of-correlated
Jun 2, 2026
Merged

TML-2729: drop LATERAL include codegen for correlated-only read path#667
aqrln merged 3 commits into
mainfrom
tml-2729-sql-orm-drop-lateral-for-includes-in-favour-of-correlated

Conversation

@tensordreams

@tensordreams tensordreams commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

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 / correlated choice. This PR removes the remaining axis.

At a glance

The whole include lowering in compileSelectWithIncludes is now a single unconditional correlated arm — no capability branch, no LATERAL builder:

for (const include of state.includes) {
  const artifact = buildCorrelatedIncludeProjection(contract, tableName, include);
  includeProjection.push(artifact.projection);
}

Previously this loop branched on selectIncludeStrategy(contract): a lateral-capable target (Postgres) went through buildLateralIncludeArtifacts (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:

  1. Deletes buildLateralIncludeArtifacts() and include-strategy.ts (selectIncludeStrategy + the IncludeStrategy type).
  2. Drops the strategy: 'lateral' | 'correlated' parameter from every include builder in the chain, so the correlated path is unconditional.
  3. Renames compileSelectWithIncludeStrategycompileSelectWithIncludes (it no longer selects anything).
  4. Keeps the four independent LATERAL consumers untouched: the lateral capability flag, JoinAst.lateral, the Postgres renderer's LATERAL emission, and the public lateralJoin() / 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 inner Aggregate → Limit → Index Scan, same buffers, same loop counts. The per-parent LIMIT forbids 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

  1. Collapse the dispatch. collection-dispatch.ts no longer imports or calls selectIncludeStrategy; dispatchWithIncludes calls the renamed compileSelectWithIncludes directly. The stale "lateral / correlated builders pick on the capability flag" comments are reworded to describe the single correlated single-query builder.

  2. Unconditional correlated lowering. In query-plan-select.ts the strategy parameter is removed from buildNestedIncludeArtifacts, buildIncludeChildRowsSelect, buildDistinctNonLeafChildRowsSelect, buildIncludeChildCombineSelect, buildIncludeChildCombineBranchSelect, and buildIncludeChildRowsAggregateSelect. buildNestedIncludeArtifacts is simplified to return projections only (correlated emits no joins), which lets the child SELECT drop its now-always-empty .withJoins(nestedJoins) wiring.

  3. Delete the dead surface. buildLateralIncludeArtifacts, selectIncludeStrategy, the IncludeStrategy type, and their test file are removed; the re-export in query-plan.ts is renamed. A repo-wide grep for all four symbols (excluding dist) returns zero hits.

  4. 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: a lateral-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.ts rewrites its two former lateral-join assertions to assert the correlated-subquery shape (no lateral join, no LATERAL keyword).

  • The lateral capability flag is now inert for include codegen. Advertising lateral: true no longer changes the emitted include SQL. Evidence: a dedicated guard in test/sql-orm-client/nested-includes-strategy.test.ts asserts a lateral-capable contract resolves a depth-2 include in one execution, ast.joins contains no .lateral join, and the lowered SQL contains no LATERAL keyword.

  • Single-execution (no N+1) guarantee preserved at every depth. compileSelectWithIncludes still 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) in nested-includes-strategy.test.ts.

Reviewer notes

  • The largest diff is test deletion, not logic change. query-plan-select.test.ts shrinks by ~560 lines: the lateral-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.ts deviation. Two of its tests asserted join.lateral === true for 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).
  • includeJoins is not dead. After removing the lateral arm from compileSelectWithIncludes, includeJoins is still populated by MTI/polymorphism variant joins and flows into the AST — it's just no longer fed by includes. The JoinAst import remains in genuine use (combine branches still use JoinAst.inner).
  • One doc touch outside the package. projects/middleware-intercept-and-cache/follow-ups.md named the renamed symbol; updated to compileSelectWithIncludes.
  • Upgrade-coverage entry. Because the PR touches packages/3-extensions/, the coverage gate requires an extension-author upgrade directory for the in-flight transition. Added skills/extension-author/prisma-next-extension-upgrade/upgrades/0.12-to-0.13/instructions.md with changes: [] — none of the touched sql-orm-client symbols are in the package's public exports/index.ts, so there's no extension-author-facing change. (The branch was also merged up to current main at 0.12.0.)
  • Second commit retires stale includeMany references surfaced during review. includeMany is no longer a method on any surface (the ORM exposes .include(...); the SQL builder has no include method). This commit deletes the obsolete include-many-patterns.mdc rulecard, removes the dead HasIncludeManyCapabilities type, aligns the Postgres/SQLite adapter READMEs with correlated lowering (Postgres no longer documents LEFT JOIN LATERAL for includes), fixes a capability-gating example in AGENTS.md, the queries-skill gotcha, and a stale e2e describe label, and removes a broken .includeMany() SQL-builder example from docs/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, the lateral capability flag, and the lateralJoin() 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-client package tests — 485 passed, 3 skipped.
  • @prisma-next/integration-tests test/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.ts throws an uncaught Connection terminated (needs a live PG that drops in the sandbox) and pollutes a few CLI test files in the same test:packages worker — those CLI files pass in isolation; and the test/integration/cli-journeys/*.e2e.test.ts suites fail on pnpm 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

  • The ticket notes one genuinely faster plan for the no-take, large-fanout case (a GROUP BY pre-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.md and docs/architecture docs/subsystems/3. Query Lanes.md carry broader staleness than includeMany — they document a db.sql.from(...) / db.schema.tables SQL-builder surface that no longer exists. Left for a dedicated doc audit rather than rewritten piecemeal here. The three ADRs that mention includeMany are left as point-in-time historical records.

Alternatives considered

  • Keep LATERAL for Postgres. Rejected: the benchmark shows no plan or latency advantage for the include shapes we emit, so the second builder was pure maintenance cost.
  • Hardwire 'correlated' but keep the strategy parameter 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.
  • Adopt the GROUP BY pre-aggregate plan for includes. Rejected for this PR: it's incompatible with per-parent LIMIT (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

  • All commits are signed off (git commit -s) per the DCO.
  • I read CONTRIBUTING.md and the change is scoped to one logical concern.
  • Tests are updated.
  • The PR title is in TML-NNNN: <sentence-case title> form.
  • The Skill update section above is filled in.

@tensordreams tensordreams requested a review from a team as a code owner June 1, 2026 13:36
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@tensordreams, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: e9a40470-4e8c-4b6d-9f30-e2b22fd64afd

📥 Commits

Reviewing files that changed from the base of the PR and between cdd5c31 and 291bb99.

📒 Files selected for processing (4)
  • .agents/rules/README.md
  • .agents/rules/include-many-patterns.mdc
  • AGENTS.md
  • docs/reference/query-patterns.md
📝 Walkthrough

Walkthrough

This 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.

Changes

Correlated Subquery Includes

Layer / File(s) Summary
Type System and Feature Removal
.agents/rules/README.md, .agents/rules/include-many-patterns.mdc, packages/2-sql/4-lanes/relational-core/src/types.ts, AGENTS.md, docs/reference/query-patterns.md
Removes includeMany feature references from agent rules index, deletes the includeMany patterns guide, removes the exported HasIncludeManyCapabilities<TContract> type utility, and eliminates includeMany query example documentation.
Strategy Selection Infrastructure Elimination
packages/3-extensions/sql-orm-client/src/include-strategy.ts, packages/3-extensions/sql-orm-client/test/include-strategy.test.ts
Deletes the IncludeStrategy type ('lateral' | 'correlated') and the selectIncludeStrategy function that conditionally chose SQL emission based on capability flags, along with all test coverage for strategy selection across capability combinations.
Query Planner Rewrite for Correlated Subqueries
packages/3-extensions/sql-orm-client/src/query-plan-select.ts, packages/3-extensions/sql-orm-client/src/query-plan.ts, packages/3-extensions/sql-orm-client/src/collection-dispatch.ts
Replaces compileSelectWithIncludeStrategy with compileSelectWithIncludes (removes strategy parameter); adds buildNestedIncludeProjections helper for recursive correlated subquery construction; removes buildLateralIncludeArtifacts and strategy-based branching throughout nested/combined include paths. Dispatcher now routes all includes through correlated subquery builder unconditionally.
Query Plan Unit Test Updates
packages/3-extensions/sql-orm-client/test/query-plan-select.test.ts, packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts, packages/3-extensions/sql-orm-client/test/rich-query-plans.test.ts
Rewrites assertions to validate correlated subquery AST structures (SubqueryExpr, projection derivations) instead of lateral join detection; removes lateral scalar reducer tests; updates all compileSelectWithIncludeStrategy calls to compileSelectWithIncludes; adjusts test comments to describe correlated JSON aggregation behavior.
Integration Test Refactor for Correlated Behavior
test/integration/test/sql-orm-client/nested-includes-strategy.test.ts, test/integration/test/sql-orm-client/include.test.ts, test/integration/test/sql-orm-client/nested-includes-helpers.ts
Removes lateral-vs-correlated cross-strategy equivalence test suites and rewrites for correlated-only coverage; replaces positive LATERAL join detection with negative assertions (no LATERAL keyword in SQL, no lateral joins in AST) and correlated subquery AST validation; removes lateral capability variants from include execution and scalar aggregate tests; clarifies in helper comments that lateral capability flag is inert for include codegen.
User-Facing Documentation and Adapter READMEs
packages/3-targets/6-adapters/postgres/README.md, packages/3-targets/6-adapters/sqlite/README.md, skills/prisma-next-queries/postgres.md, test/e2e/framework/test/sqlite/orm.test.ts
Updates adapter READMEs to describe JSON aggregation rendering and correlated scalar subqueries as first-class capabilities (not includeMany-specific features); updates documentation examples from includeMany to lateral capability references; renames SQLite e2e test describe block from includeMany to include.

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • prisma/prisma-next#555: Modifies the same nested-include planner and dispatcher code paths to change include execution strategy selection (lateral vs correlated vs multi-query).
  • prisma/prisma-next#619: Refactors SQL ORM include dispatch in the same modules by removing multi-query/strategy branching and switching to a single include compilation path.
  • prisma/prisma-next#596: Modifies the same include planning and scalar/combine lowering logic in query-plan-select.ts and collection-dispatch.ts for single-query correlated forms.

Suggested reviewers

  • aqrln
  • wmadden

Poem

🐰 Lateral joins hop away without a trace,
Correlated subqueries claim their rightful place,
One path to rule them all, clean and true,
No strategy branching—just what we do!
Aggregates nestle where SQL can see,
Simpler, faster, the way it should be.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: removing LATERAL include codegen and consolidating to a correlated-only read path, which is the central refactor across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2729-sql-orm-drop-lateral-for-includes-in-favour-of-correlated

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 135.76 KB (-0.22% 🔽)
postgres / emit 108.02 KB (-0.31% 🔽)
mongo / no-emit 75.76 KB (0%)
mongo / emit 70.75 KB (0%)
cf-worker / no-emit 164.74 KB (0%)
cf-worker / emit 133.79 KB (0%)

@pkg-pr-new

pkg-pr-new Bot commented Jun 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/@prisma-next/extension-author-tools@667

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/@prisma-next/mongo-runtime@667

@prisma-next/family-mongo

npm i https://pkg.pr.new/@prisma-next/family-mongo@667

@prisma-next/sql-runtime

npm i https://pkg.pr.new/@prisma-next/sql-runtime@667

@prisma-next/family-sql

npm i https://pkg.pr.new/@prisma-next/family-sql@667

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/@prisma-next/extension-arktype-json@667

@prisma-next/middleware-cache

npm i https://pkg.pr.new/@prisma-next/middleware-cache@667

@prisma-next/mongo

npm i https://pkg.pr.new/@prisma-next/mongo@667

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/@prisma-next/extension-paradedb@667

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/@prisma-next/extension-pgvector@667

@prisma-next/extension-postgis

npm i https://pkg.pr.new/@prisma-next/extension-postgis@667

@prisma-next/postgres

npm i https://pkg.pr.new/@prisma-next/postgres@667

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/@prisma-next/sql-orm-client@667

@prisma-next/sqlite

npm i https://pkg.pr.new/@prisma-next/sqlite@667

@prisma-next/target-mongo

npm i https://pkg.pr.new/@prisma-next/target-mongo@667

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/@prisma-next/adapter-mongo@667

@prisma-next/driver-mongo

npm i https://pkg.pr.new/@prisma-next/driver-mongo@667

@prisma-next/contract

npm i https://pkg.pr.new/@prisma-next/contract@667

@prisma-next/utils

npm i https://pkg.pr.new/@prisma-next/utils@667

@prisma-next/config

npm i https://pkg.pr.new/@prisma-next/config@667

@prisma-next/errors

npm i https://pkg.pr.new/@prisma-next/errors@667

@prisma-next/framework-components

npm i https://pkg.pr.new/@prisma-next/framework-components@667

@prisma-next/operations

npm i https://pkg.pr.new/@prisma-next/operations@667

@prisma-next/ts-render

npm i https://pkg.pr.new/@prisma-next/ts-render@667

@prisma-next/contract-authoring

npm i https://pkg.pr.new/@prisma-next/contract-authoring@667

@prisma-next/ids

npm i https://pkg.pr.new/@prisma-next/ids@667

@prisma-next/psl-parser

npm i https://pkg.pr.new/@prisma-next/psl-parser@667

@prisma-next/psl-printer

npm i https://pkg.pr.new/@prisma-next/psl-printer@667

@prisma-next/cli

npm i https://pkg.pr.new/@prisma-next/cli@667

@prisma-next/cli-telemetry

npm i https://pkg.pr.new/@prisma-next/cli-telemetry@667

@prisma-next/emitter

npm i https://pkg.pr.new/@prisma-next/emitter@667

@prisma-next/migration-tools

npm i https://pkg.pr.new/@prisma-next/migration-tools@667

prisma-next

npm i https://pkg.pr.new/prisma-next@667

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/@prisma-next/vite-plugin-contract-emit@667

@prisma-next/mongo-codec

npm i https://pkg.pr.new/@prisma-next/mongo-codec@667

@prisma-next/mongo-contract

npm i https://pkg.pr.new/@prisma-next/mongo-contract@667

@prisma-next/mongo-value

npm i https://pkg.pr.new/@prisma-next/mongo-value@667

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/@prisma-next/mongo-contract-psl@667

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/@prisma-next/mongo-contract-ts@667

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/@prisma-next/mongo-emitter@667

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/@prisma-next/mongo-schema-ir@667

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/@prisma-next/mongo-query-ast@667

@prisma-next/mongo-orm

npm i https://pkg.pr.new/@prisma-next/mongo-orm@667

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/@prisma-next/mongo-query-builder@667

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/@prisma-next/mongo-lowering@667

@prisma-next/mongo-wire

npm i https://pkg.pr.new/@prisma-next/mongo-wire@667

@prisma-next/sql-contract

npm i https://pkg.pr.new/@prisma-next/sql-contract@667

@prisma-next/sql-errors

npm i https://pkg.pr.new/@prisma-next/sql-errors@667

@prisma-next/sql-operations

npm i https://pkg.pr.new/@prisma-next/sql-operations@667

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/@prisma-next/sql-schema-ir@667

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/@prisma-next/sql-contract-psl@667

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/@prisma-next/sql-contract-ts@667

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/@prisma-next/sql-contract-emitter@667

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/@prisma-next/sql-lane-query-builder@667

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/@prisma-next/sql-relational-core@667

@prisma-next/sql-builder

npm i https://pkg.pr.new/@prisma-next/sql-builder@667

@prisma-next/target-postgres

npm i https://pkg.pr.new/@prisma-next/target-postgres@667

@prisma-next/target-sqlite

npm i https://pkg.pr.new/@prisma-next/target-sqlite@667

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/@prisma-next/adapter-postgres@667

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/@prisma-next/adapter-sqlite@667

@prisma-next/driver-postgres

npm i https://pkg.pr.new/@prisma-next/driver-postgres@667

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/@prisma-next/driver-sqlite@667

commit: 291bb99

Comment thread packages/3-extensions/sql-orm-client/src/query-plan-select.ts Outdated
Comment thread packages/3-targets/6-adapters/postgres/README.md Outdated
Comment thread packages/3-targets/6-adapters/postgres/README.md Outdated
Comment thread packages/3-targets/6-adapters/sqlite/README.md Outdated
Comment thread test/integration/test/sql-orm-client/include.test.ts Outdated
…-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>
@tensordreams tensordreams force-pushed the tml-2729-sql-orm-drop-lateral-for-includes-in-favour-of-correlated branch from 72db66a to 291bb99 Compare June 2, 2026 12:53
@aqrln aqrln merged commit e939a7a into main Jun 2, 2026
21 checks passed
@aqrln aqrln deleted the tml-2729-sql-orm-drop-lateral-for-includes-in-favour-of-correlated branch June 2, 2026 13:10
paulwer pushed a commit to paulwer/prisma-next that referenced this pull request Jun 11, 2026
…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>
paulwer pushed a commit to paulwer/prisma-next that referenced this pull request Jun 15, 2026
…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>
paulwer pushed a commit to paulwer/prisma-next that referenced this pull request Jun 21, 2026
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>
paulwer pushed a commit to paulwer/prisma-next that referenced this pull request Jun 21, 2026
…–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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants