Skip to content

TML-2785: M:N slice 1 — correlated include read through the junction#679

Merged
aqrln merged 19 commits into
mainfrom
tml-2785-slice-1-correlated-read
Jun 10, 2026
Merged

TML-2785: M:N slice 1 — correlated include read through the junction#679
aqrln merged 19 commits into
mainfrom
tml-2785-slice-1-correlated-read

Conversation

@tensordreams

@tensordreams tensordreams commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Slice 1 of the SQL ORM: Many-to-Many End to End project (Linear project). Reads an M:N relation through its junction.

Stacked PR. Base is tml-2784 (#678, slice 0) → tml-2597 (#673) → tml-2729 (#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.

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.

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

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 9751f5f3-be80-4599-9dc7-28e7b95c6ce7

📥 Commits

Reviewing files that changed from the base of the PR and between 9c9572e and 69f487a.

⛔ Files ignored due to path filters (10)
  • projects/sql-orm-many-to-many/learnings.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/dispatches/01-fixture-m2n.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/dispatches/02-read-path.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/dispatches/02-read-path.r2.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/dispatches/02-read-path.r3.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/dispatches/03-integration-tests.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/plan.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/spec.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/spec.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/trace.jsonl is excluded by !projects/**
📒 Files selected for processing (9)
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/3-extensions/sql-orm-client/src/collection-dispatch.ts
  • packages/3-extensions/sql-orm-client/src/collection.ts
  • packages/3-extensions/sql-orm-client/src/query-plan-select.ts
  • packages/3-extensions/sql-orm-client/src/types.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-select.test.ts
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.13-to-0.14/instructions.md
  • test/integration/test/sql-orm-client/mn-include.test.ts
  • test/integration/test/sql-orm-client/runtime-helpers.ts
✅ Files skipped from review due to trivial changes (1)
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.13-to-0.14/instructions.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/3-extensions/sql-orm-client/src/collection.ts
  • packages/3-extensions/sql-orm-client/src/types.ts
  • test/integration/test/sql-orm-client/runtime-helpers.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-select.test.ts
  • test/integration/test/sql-orm-client/mn-include.test.ts
  • packages/3-extensions/sql-orm-client/src/query-plan-select.ts

📝 Walkthrough

Walkthrough

Adds many-to-many include support to the SQL ORM client by introducing junction-table descriptors in types and relation resolution, propagating them through include expressions, and lowering includes to correlated subqueries with junction joins; includes unit and integration tests covering single/composite keys and distinct+nested scenarios.

Changes

Many-to-many include support

Layer / File(s) Summary
Type contracts for through-table metadata
packages/3-extensions/sql-orm-client/src/types.ts
Introduces exported IncludeThroughDescriptor and extends IncludeExpr with optional through to carry junction metadata.
Contract resolution for M:N relations
packages/3-extensions/sql-orm-client/src/collection-contract.ts
Detects relation.through, builds a typed through descriptor (copying table/namespace/columns) and derives parentLocalColumns from relation.on.localFields.
Include expression propagation
packages/3-extensions/sql-orm-client/src/collection.ts
Collection.include() conditionally spreads the resolved through descriptor into the generated includeExpr.
Dispatch join-column selection
packages/3-extensions/sql-orm-client/src/collection-dispatch.ts
dispatchWithIncludes derives parentJoinColumns preferring include.through.parentLocalColumns when present, otherwise include.localColumn.
Query planning for M:N includes and junction logic
packages/3-extensions/sql-orm-client/src/query-plan-select.ts
Adds buildManyToManyJunctionArtifacts to validate keys and build correlated junction WHERE and child↔junction joins; buildIncludeChildRowsSelect branches on include.through to use junction artifacts or direct FK predicates and threads junctionJoins through distinct non-leaf lowering.
Unit test SQL-plan assertions
packages/3-extensions/sql-orm-client/test/query-plan-select.test.ts
Adds M:N include correlated subquery tests and helpers asserting SQL-plan shape for single/composite M:N, FK control case, and distinct+nested-include regression.
Integration test schema and helpers
test/integration/test/sql-orm-client/runtime-helpers.ts
Creates user_tags junction table, adds SeedTag/SeedUserTag types and exported seedTags/seedUserTags helpers.
End-to-end M:N include integration tests
test/integration/test/sql-orm-client/mn-include.test.ts
Adds integration tests verifying include('tags') end-to-end behavior, single captured SQL execution, empty arrays for missing associations, shared tags, nested M:N under 1:N, and sibling include composition.
Release notes documentation
skills/extension-author/prisma-next-extension-upgrade/upgrades/0.13-to-0.14/instructions.md
Notes internal runtime change enabling N:M correlated .include() reads via junction tables; explicit that API/contract unchanged and no author action required.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • prisma/prisma-next#667: Modifies query-plan-select.ts in the correlated-subquery include lowering path overlapping distinct non-leaf lowering.
  • prisma/prisma-next#678: Adds groundwork for relation.through metadata population that this PR builds upon.

Suggested reviewers

  • aqrln

"🐰 With junctions mapped through and queries woven tight,
M:N relations bloom in correlated light—
No more one path, now forests intertwine,
Each tag to user, a perfect aligned design! ✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: implementing M:N (many-to-many) correlated include read through a junction table, matching the core purpose of all the code changes.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2785-slice-1-correlated-read

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 150.96 KB (+0.28% 🔺)
postgres / emit 119.72 KB (+0.35% 🔺)
mongo / no-emit 76.67 KB (0%)
mongo / emit 70.96 KB (0%)
cf-worker / no-emit 179.99 KB (0%)
cf-worker / emit 145.48 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@679

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/extension-supabase

npm i https://pkg.pr.new/@prisma-next/extension-supabase@679

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 69f487a

@tensordreams tensordreams force-pushed the tml-2785-slice-1-correlated-read branch from 4ee6af6 to 78affcf Compare June 2, 2026 12:39
@tensordreams tensordreams force-pushed the tml-2784-slice-0-contract-resolver-foundation branch from b3bba1c to e44f8aa Compare June 2, 2026 13:07
@tensordreams tensordreams force-pushed the tml-2785-slice-1-correlated-read branch from 78affcf to e4e3004 Compare June 2, 2026 13:07
@tensordreams tensordreams force-pushed the tml-2784-slice-0-contract-resolver-foundation branch from e44f8aa to aaefe75 Compare June 3, 2026 08:51
@tensordreams tensordreams force-pushed the tml-2785-slice-1-correlated-read branch 2 times, most recently from 9e24a8b to ed06084 Compare June 3, 2026 11:32
@tensordreams tensordreams force-pushed the tml-2784-slice-0-contract-resolver-foundation branch 2 times, most recently from 87de795 to 4763690 Compare June 4, 2026 15:12
@tensordreams tensordreams force-pushed the tml-2785-slice-1-correlated-read branch 2 times, most recently from 38bcb74 to c32cd78 Compare June 4, 2026 15:41
@tensordreams tensordreams force-pushed the tml-2784-slice-0-contract-resolver-foundation branch from d15ad0b to 5450121 Compare June 5, 2026 12:44
@tensordreams tensordreams force-pushed the tml-2785-slice-1-correlated-read branch 4 times, most recently from 7e21c60 to f383943 Compare June 8, 2026 10:19
@tensordreams tensordreams force-pushed the tml-2784-slice-0-contract-resolver-foundation branch 2 times, most recently from 67f9537 to 2aaf8c0 Compare June 8, 2026 14:46
@tensordreams tensordreams force-pushed the tml-2785-slice-1-correlated-read branch from a36b0d5 to 07aa1cf Compare June 8, 2026 14:46
@tensordreams tensordreams force-pushed the tml-2784-slice-0-contract-resolver-foundation branch from 2aaf8c0 to cf13cac Compare June 8, 2026 16:45
@tensordreams tensordreams force-pushed the tml-2785-slice-1-correlated-read branch from 07aa1cf to 12ec127 Compare June 8, 2026 16:45
Base automatically changed from tml-2784-slice-0-contract-resolver-foundation to main June 8, 2026 17:58
@tensordreams tensordreams force-pushed the tml-2785-slice-1-correlated-read branch from 12ec127 to f4f4d0b Compare June 9, 2026 11:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/3-extensions/sql-orm-client/src/query-plan-select.ts`:
- Around line 367-372: Add explicit fail-fast length checks instead of using the
fallback indexing in the M:N junction logic: before computing joinOnPairs (the
BinaryExpr.eq calls that use ColumnRef.of(junctionTable, junctionCol) and
ColumnRef.of(childTableRef, targetColumns[i] ?? junctionCol)), assert
childColumns.length === targetColumns.length and similarly assert
parentColumns.length === parentLocalColumns.length (or the actual parent-side
variable names used where parentLocalColumns are indexed). Throw a clear Error
with a descriptive message (e.g., "M:N junction: childColumns and targetColumns
length mismatch") so misaligned through descriptor arrays fail fast rather than
silently falling back.

In
`@skills/extension-author/prisma-next-extension-upgrade/upgrades/0.12-to-0.13/instructions.md`:
- Around line 55-59: Update the paragraph that claims full M:N support so it
accurately reflects this PR implements only the read-only slice: change wording
to state that TML-2785 implements correlated include reads (see
buildManyToManyJunctionArtifacts and include("tags") tests), and remove or
separate claims about EXISTS-through-junction filters and junction-table nested
writes (connect/disconnect/create) which belong to TML-2786/TML-2787
write/filter slices; explicitly note that write/filter functionality is planned
in those separate tickets and no extension-author action is required for the
current read-only runtime change.

In `@test/integration/test/sql-orm-client/mn-include.test.ts`:
- Line 106: The assertions checking for absence of "LATERAL" are case-sensitive;
update them to be case-insensitive by changing the checks on
runtime.executions[0]?.sql (and the similar check around line 279) to use a
case-insensitive match — e.g. replace .not.toContain('LATERAL') with
.not.toMatch(/lateral/i) or transform the SQL to one case and check
(.toUpperCase().not.toContain('LATERAL') or
.toLowerCase().not.toContain('lateral')) so both "LATERAL" and "lateral" are
caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 83c5097d-c7ab-45bc-a8be-19c90fd0f633

📥 Commits

Reviewing files that changed from the base of the PR and between b5c38a0 and f4f4d0b.

⛔ Files ignored due to path filters (10)
  • projects/sql-orm-many-to-many/learnings.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/dispatches/01-fixture-m2n.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/dispatches/02-read-path.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/dispatches/02-read-path.r2.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/dispatches/02-read-path.r3.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/dispatches/03-integration-tests.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/plan.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/01-correlated-read-through-junction/spec.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/spec.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/trace.jsonl is excluded by !projects/**
📒 Files selected for processing (10)
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/3-extensions/sql-orm-client/src/collection-dispatch.ts
  • packages/3-extensions/sql-orm-client/src/collection.ts
  • packages/3-extensions/sql-orm-client/src/query-plan-select.ts
  • packages/3-extensions/sql-orm-client/src/types.ts
  • packages/3-extensions/sql-orm-client/test/helpers.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-select.test.ts
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.12-to-0.13/instructions.md
  • test/integration/test/sql-orm-client/mn-include.test.ts
  • test/integration/test/sql-orm-client/runtime-helpers.ts

Comment thread packages/3-extensions/sql-orm-client/src/query-plan-select.ts Outdated
Comment thread test/integration/test/sql-orm-client/mn-include.test.ts Outdated
Comment thread packages/3-extensions/sql-orm-client/src/collection-contract.ts Outdated
@tensordreams tensordreams force-pushed the tml-2785-slice-1-correlated-read branch 2 times, most recently from 9c9572e to ce66bba Compare June 9, 2026 17:21
@aqrln aqrln enabled auto-merge June 10, 2026 14:21
…ndard (TML-2785)

Read slice (correlated include through junction). Bakes the operator
integration-test standard (whole-row asserts, explicit select, +implicit
nested-M:N case) into the project cross-cutting requirements and slice 1.
3 dispatches: fixture M:N / read code / integration tests.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Add many-to-many include support via a single correlated subquery that
joins the child table to the junction on junction.childColumns = child.targetColumns
and correlates to the parent via WHERE junction.parentColumns = parent.parentLocalColumns.
Composite keys AND across all column pairs. No LATERAL joins.

- IncludeExpr gains `through?: IncludeThroughDescriptor` carrying junction table
  name, parentColumns, childColumns, targetColumns, and parentLocalColumns.
- `resolveIncludeRelation` in collection-contract.ts surfaces `through` from the
  contract relation when present, resolving field names to column names for the
  parent local columns.
- `Collection.include()` propagates `through` into IncludeExpr via spread.
- `buildManyToManyJunctionArtifacts` in query-plan-select.ts builds the JOIN ON
  expression (BinaryExpr or AndExpr over child column pairs) and the correlated
  WHERE (BinaryExpr or AndExpr over parent column pairs), producing a non-lateral
  inner JoinAst to the junction table.
- `buildIncludeChildRowsSelect` detects `include.through` and uses the M:N
  artifacts instead of the FK equality WHERE; `buildDistinctNonLeafChildRowsSelect`
  receives and applies the same junction joins.
- `dispatchWithIncludes` in collection-dispatch.ts forces all `through.parentLocalColumns`
  (not just `localColumn`) into the parent SELECT augmentation for composite M:N keys.
- `buildManyToManyContract` test helper and M:N unit tests covering single-column
  and composite-key junction shapes, plus a FK path non-regression test.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…ManyJunctionArtifacts; add M:N+distinct+non-leaf test

F1: the two bare `as AnyExpression` casts in `buildManyToManyJunctionArtifacts` are replaced with
`castAs<AnyExpression>(…!)` — BinaryExpr is a union member of AnyExpression, so the assertion is
type-checked; the non-null assertion is safe because the branch is only taken when `length === 1`.
Adds the `castAs` import from `@prisma-next/utils/casts`. `lint:casts` delta: -1.

F2: new unit test `attaches junction join to baseInner in M:N + distinct + nested non-leaf path`
in the `M:N include correlated subquery` describe block. Constructs a contract with
parents→children (M:N via parent_child junction) + children→grandchildren (FK), sets
`distinct: ['name']` and a nested grandchild include on the M:N IncludeExpr, calls
`compileSelectWithIncludes`, and asserts:
  - junction join (`INNER JOIN parent_child`, `lateral: false`) attaches to `baseInner`
    (the innermost scalar SELECT inside the ROW_NUMBER wrap), not to the dedup wrapper or
    outer distinct SELECT
  - correlated WHERE (`parent_child.parent_id = parents.id`) is present at `baseInner`
  - no junction join leaks to `innerSelect` or the outer `childRows`

All 493 tests pass; typecheck clean.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Adds `mn-include.test.ts` covering the end-to-end M:N include path for
`User.tags` via the `user_tags` junction, using the PGlite harness.

Tests satisfy the operator integration-test standard:
- Whole-row toEqual assertions on every test
- Explicit .select() on 6/7 tests
- One implicit/default-selection test (full User + tags: Tag[] shape)
- Single-execution assertion + no LATERAL in emitted SQL
- Depth-2: M:N tags nested under invitedUsers (1:N self-relation)
- Sibling depth-2: include("tags") alongside include("posts") in one execution
- Edge: user with no tags returns tags: []
- Edge: tag shared by multiple users resolves independently for each

Also extends `setupTestSchema` and adds `seedTags`/`seedUserTags` helpers
to `runtime-helpers.ts` to support the junction table in integration tests.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
… (TML-2785)

Orchestrator artifacts for the read slice (fixture / read-path / integration
dispatches; read-path took 3 rounds incl. a truncation recovery). Review log
under reviews/ is gitignored.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…-2605 rebase

Carry the resolved junction namespaceId onto IncludeThroughDescriptor and
build the correlated include's junction TableSource with it, so the read
path emits namespace-qualified SQL for the junction like the rest of the
runtime after TML-2605.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…ough descriptor

The contract now carries the junction's namespaceId on through (slice 0);
the include descriptor passes it through as a required field instead of
re-deriving it from storage. Re-emit the M:N fixture so the User<->Tag
through block carries it.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
… M:N contracts (TML-2808 rebase)

Main moved namespace tables under entries.table; the hand-built M:N test
contracts still used the flat tables record, so table resolution missed
them.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…eated_at)

The shared fixture's junction declares a nullable note and a defaulted
created_at (exclusion-path coverage for requiredPayloadColumns); create
the columns so the schema matches the contract.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…junction artifacts

biome `noNonNullAssertion` (error-on-warnings) flagged the `joinOnPairs[0]!`
/ `correlationPairs[0]!` non-null assertions. Narrow with a truthy guard on
the destructured first element instead, which also removes the castAs (so
its import goes too).

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…-2841 coordinate rebase

Main un-pinned the ORM client onto ADR 221 coordinates: resolveFieldToColumn
gained a leading namespaceId, IncludeExpr requires relatedNamespaceId, and
compileSelectWithIncludes takes (contract, namespaceId, table, state). Adapt
the M:N parentLocalColumns resolution and the read-path tests accordingly.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…tions

Slice 0 (the contract-shape change) merged into main, so its instructions
touch is no longer in the open PRs diff — but slices 1-3 still touch
packages/3-extensions/, which the check:upgrade-coverage gate requires be
accompanied by an instructions touch. Record the M:N runtime slices as
incidental for extension authors (no API/contract change beyond TML-2784).

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Replace the silent `?? junctionCol` fallback indexing in
buildManyToManyJunctionArtifacts with explicit length invariants over the
through descriptor's column arrays (childColumns/targetColumns and
parentColumns/parentLocalColumns), then index with assertDefined narrowing
so a misalignment throws a clear error instead of emitting a wrong join or
correlation predicate.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…spread

Replace the manual `...(x !== undefined ? { through: x } : {})` spreads in
resolveIncludeRelation and the include builder with the ifDefined helper from
@prisma-next/utils/defined, per the use-if-defined convention.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Migrate the M:N include query-plan unit tests off the hand-rolled
buildManyToManyContract helper and onto the real emitted fixture's relations:
User.tags via user_tags (single-column), Project.related via project_links
(composite, self-referential), and the distinct + nested-non-leaf lowering via
Project.related distinct('name').include('related'). Each test now asserts the
whole compiled plan.ast with a single toEqual and drops the lateral / 'no
LATERAL' assertions (there is only one strategy). buildManyToManyContract (and
its RawColumn type) had no remaining uses on this branch and is removed.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…M:N include

Drop the SQL-string LATERAL assertions from the M:N include integration tests.
LATERAL can't appear (no such strategy exists in code), so the assertion tested
an implementation detail. The real non-functional invariant — the include
resolves in a single SQL execution — is already asserted via
runtime.executions length.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The 0.12->0.13 extension-author note overstated delivered functionality: it
claimed EXISTS-through-junction filters (TML-2786) and junction nested writes
(TML-2787) alongside the correlated include read. This slice is read-only, so
narrow the note to TML-2785's M:N correlated include read; the filter and write
paths are documented in their own slices.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
…suite

The test asserting that a 1:N FK include still lowers without a junction
join carried an opaque name ("FK include path is unchanged (no
regression)") that didn't say what it checks or how it relates to M:N.
Rename it to state the asserted lowering and add a comment marking it as
the control case for the junction-join branch — it pins that the branch
fires only for N:M relations and never leaks into the FK include path.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
0.13.0 shipped without the sql-orm-client M:N runtime, so the note
describing the correlated include reads belongs in the 0.13→0.14
extension-author instructions, not the now-released 0.12→0.13 doc. The
upgrade-coverage gate targets 0.13→0.14 after the release bump.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
@tensordreams tensordreams force-pushed the tml-2785-slice-1-correlated-read branch from ce66bba to 69f487a Compare June 10, 2026 14:26
@aqrln aqrln added this pull request to the merge queue Jun 10, 2026
Merged via the queue into main with commit e7ce035 Jun 10, 2026
21 checks passed
@aqrln aqrln deleted the tml-2785-slice-1-correlated-read branch June 10, 2026 14:50
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>
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