Skip to content

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

Open
tensordreams wants to merge 2 commits into
mainfrom
tml-2729-sql-orm-drop-lateral-for-includes-in-favour-of-correlated
Open

TML-2729: drop LATERAL include codegen for correlated-only read path#667
tensordreams wants to merge 2 commits into
mainfrom
tml-2729-sql-orm-drop-lateral-for-includes-in-favour-of-correlated

Conversation

@tensordreams
Copy link
Copy Markdown
Contributor

@tensordreams tensordreams commented Jun 1, 2026

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

…-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>
@tensordreams tensordreams requested a review from a team as a code owner June 1, 2026 13:36
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

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 17 minutes and 49 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: e9f2aacd-ebd2-44a9-892d-a2b72776503d

📥 Commits

Reviewing files that changed from the base of the PR and between 82216b8 and aa68bf5.

⛔ Files ignored due to path filters (1)
  • projects/middleware-intercept-and-cache/follow-ups.md is excluded by !projects/**
📒 Files selected for processing (20)
  • .agents/rules/README.md
  • .agents/rules/include-many-patterns.mdc
  • AGENTS.md
  • docs/reference/query-patterns.md
  • packages/2-sql/4-lanes/relational-core/src/types.ts
  • packages/3-extensions/sql-orm-client/src/collection-dispatch.ts
  • packages/3-extensions/sql-orm-client/src/include-strategy.ts
  • 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/test/collection-dispatch.test.ts
  • packages/3-extensions/sql-orm-client/test/include-strategy.test.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-select.test.ts
  • packages/3-extensions/sql-orm-client/test/rich-query-plans.test.ts
  • 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
  • test/integration/test/sql-orm-client/include.test.ts
  • test/integration/test/sql-orm-client/nested-includes-helpers.ts
  • test/integration/test/sql-orm-client/nested-includes-strategy.test.ts
✨ 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
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

size-limit report 📦

Path Size
postgres / no-emit 135.63 KB (-0.23% 🔽)
postgres / emit 125.29 KB (-0.25% 🔽)
mongo / no-emit 75.69 KB (0%)
mongo / emit 70.68 KB (0%)

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 1, 2026

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: aa68bf5

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

1 participant