Skip to content

TML-2683: wire polymorphism into the SQL ORM .include() child path#669

Open
tensordreams wants to merge 26 commits into
tml-2729-sql-orm-drop-lateral-for-includes-in-favour-of-correlatedfrom
tml-2683-sql-orm-include-silently-degrades-on-polymorphic-target
Open

TML-2683: wire polymorphism into the SQL ORM .include() child path#669
tensordreams wants to merge 26 commits into
tml-2729-sql-orm-drop-lateral-for-includes-in-favour-of-correlatedfrom
tml-2683-sql-orm-include-silently-degrades-on-polymorphic-target

Conversation

@tensordreams
Copy link
Copy Markdown
Contributor

@tensordreams tensordreams commented Jun 1, 2026

Draft — stacked on the TML-2729 branch (correlated-only read path). Base retargets to main once 2729 merges.

What

Fixes TML-2683: db.orm.<parent>.include('<rel>') silently degraded when <rel>'s target model is polymorphic — STI returned base-shaped rows (variant fields dropped), MTI returned rows missing variant columns. Wires the parent-side polymorphism machinery into the child include path, end-to-end, including depth-2 nested includes.

Outcome

.include('<polyRel>') returns rows shaped per each row's variant (STI + MTI); .include('<polyRel>', r => r.variant('X')) narrows at runtime and in the type; a variant-specific where filters correctly for STI (base-table) and MTI (variant-table) columns; and a nested .include() through a poly target stitches the grandchild (no depth-2 null degradation).

Dispatches (drive build-workflow; implementer + reviewer subagents — all SATISFIED)

  • D1 SQL side: child SELECT emits MTI variant joins + variant/discriminator projection. (bb42ab153)
  • D2 Decode side: decodeIncludePayload maps poly child rows via mapPolymorphicRow. (20b963758)
  • D3 .variant() narrowing + result type (bare include types as the variant union). (4c8f2d460)
  • D4 Integration (PGlite): STI/MTI includes, narrowing, STI variant-where. (34becbd8a)
  • D5 MTI variant-where: variant-aware predicate accessor. (d5d9204b4)
  • D6 Harden polymorphism.test.ts: whole-shape toEqual, STI/MTI implicit-default tests, de-raw. (abbaacd77)
  • D7 MTI+relationship coverage: MTI-as-parent, N:1 poly target, 2+ MTI variants, nested-through-poly, relationship implicit-default. (c41940f73)
  • D8 Fix depth-2 nested-include-through-poly decode (grandchild → null); unskip D7 scenario. (67d99103a)

Acceptance: 7/7 ACs PASS (0 FAIL, 0 open). Whole-slice DoD green at tip: workspace typecheck, build, package tests, PGlite integration, lint:deps.

Also adds a test-style rule: .agents/rules/sql-orm-client-whole-shape-assertions.mdc (whole-result toEqual + explicit select). Slice spec / plan / design-notes in projects/tml-2683/.

Follow-ups (filed; out of scope here)

  • TML-2782orderBy on a variant-narrowed collection drops MTI variant-field columns.
  • TML-2783 — explicit .select(...) on a poly include doesn't restrict variant-table columns (filed in May WS2).
  • SQLite poly-include integration coverage — subsumed by the multi-target test-runtime project; some SQLite breakage accepted through mid-July (Postgres GA focus).

Add slice spec, dispatch plan, and design notes for wiring polymorphism
into the SQL ORM .include() child path. Planned against the correlated-only
read path (post TML-2657 + TML-2729): single correlated include builder,
MTI variant joins re-introduced into the child SELECT, decode via
mapPolymorphicRow, and .variant() narrowing on include refinements.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: c2cae35e-83ac-4808-980d-023711d2d5d2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2683-sql-orm-include-silently-degrades-on-polymorphic-target

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.

…tion

Add parent->poly relations to the unit poly contracts (Project->Task MTI,
Account->User STI, Task->Task self relation) and assert the correlated child
SELECT emits MTI variant joins, variant_table__column projection, the
discriminator/STI base-table columns, variant-narrowed inner join, and the
self-relation alias remap on the variant join ON.
… child SELECT

For an .include() whose target model is polymorphic, the correlated child
SELECT now mirrors the parent path: resolve the target's PolymorphismInfo and,
when MTI variants exist, join the variant tables into the child subquery's FROM
(re-introducing .withJoins as the sole join source) and project their
variant_table__column cells. Inner-join the named variant when
nested.variantName is set, else left-join all variants. The discriminator and
STI variant-specific columns reach the row through the base projection. Self
relations remap the variant join ON to the child alias, mirroring the existing
orderBy/where remap. Both child-SELECT builders (plain + distinct-non-leaf)
carry the joins/projection.
…ew task columns

buildMixedPolyContract now adds project_id/parent_id columns to tasks (to host
the Project->Task and Task->Task self relations). Update the parent-side
compileSelect MTI projection expectations to match the full base column set.
…variant

Assert decodeIncludePayload maps included child rows of a polymorphic
target to their variant shape: STI rows resolve by discriminator (matching
variant field kept, other variant's NULL column stripped), MTI rows surface
variant_table__column cells under their model field names, and a
variant-narrowed include maps every child row via the named variant.
…lymorphicRow

decodeIncludePayload mapped polymorphic-target included child rows with the
base mapper, so STI rows came back base-shaped (variant fields dropped) and
MTI rows lacked their variant columns. Resolve the related model's
PolymorphismInfo once per include and, when polymorphic, map each child row
via mapPolymorphicRow (passing nested.variantName for variant-narrowed
includes); otherwise keep mapStorageRowToModelFields. This is the decode half
of the parent-child symmetry the parent dispatchers already have. Nested
recursion, scalar, combine, and empty-relation handling are unchanged.
…not the typed builder

The poly contracts patch Account/Project/Task models in at runtime, so they
are absent from the generated fixture's Models type; createCollectionFor's
ModelName constraint rejected them and tsc failed. Construct the IncludeExpr
directly from resolveIncludeRelation and dispatch with string model/table
names — the same helper the plan-level poly tests already use — so the decode
tests typecheck. Same assertions; variant narrowing now sets nested
variantName through the include builder rather than post-hoc mapping.
…riant() narrows

Type tests: .include('<polyRel>') without refinement yields the variant union
row type; .include('<polyRel>', r => r.variant('X')) narrows the included value
to variant X's row. Runtime tests: r.variant('X') on an include refinement sets
nested.variantName; a bare include leaves it unset.
…n and .variant()

include() typed the included relation off DefaultModelRow (base fields only),
so a polymorphic-target include silently dropped variant fields at the type
level. Type the default included row off InferRootRow — the variant union —
mirroring the root collection's default row, so a bare include surfaces the
union and r.variant('X') in a refinement narrows to variant X's row. The
runtime already threads nested.variantName through the refinement collection;
no runtime change is needed.
…n a real DB

Cover .include('<polyRel>') where the related model is polymorphic, against
the PGlite (Postgres) integration harness: STI-target include returns each
child row shaped per its discriminator variant; MTI-target include surfaces
the joined variant table's columns; a variant-specific where on the include
refinement filters correctly; .variant('X') narrows a poly include to that
variant only, for both STI and MTI targets.

The parent-bearing poly contracts (Account->members->User STI, Project->tasks
->Task Bug-STI/Feature-MTI) are built locally so the shared poly helpers stay
stable for sibling tests whose hand-rolled DDL omits the parent FK column.
…t-field where

A variant-specific `where` on an MTI polymorphic include
(`include('tasks', t => t.variant('Feature').where(x => x.priority.gte(3)))`)
threw `TypeError: Cannot read properties of undefined` because the predicate
accessor resolved fields against the base table only, while MTI variant
columns live on the joined variant table.

Thread the selected `variantName` into `createModelAccessor`. When set,
MTI variant-owned fields resolve to a `ColumnRef` qualified against the
variant table that the read path already inner-joins into the correlated
child SELECT; base fields and the no-variant path are unchanged. The `where`
predicate-accessor type now exposes the selected variant's fields via
`VariantAwareModelAccessor`, gated on the type-level `variantName`.

STI variant columns ride the base table and were already covered; only the
MTI gap is closed here.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

size-limit report 📦

Path Size
postgres / no-emit 135.89 KB (+0.2% 🔺)
postgres / emit 125.53 KB (+0.2% 🔺)
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@669

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 67d9910

Refactor polymorphism-include.test.ts to assert the entire result shape
with toEqual and explicit .select(...) projections on both the parent
collection and the included poly relation, ordering deterministically by
the base id column. Replaces partial toMatchObject/toHaveProperty/lone
toBe matchers, conforming to the sql-orm-client-whole-shape-assertions
rule.

Locks the empirically-confirmed select-vs-poly composition: STI variant
fields are base columns that select projects and mapPolymorphicRow drops
per sibling variant; MTI variant columns (features.priority) are
joined+projected regardless of select.
Replace partial matchers (toMatchObject / toHaveProperty / lone toBe) with
whole-result toEqual, ordered deterministically by base id. Convert the base /
variant queries into explicit implicit-default-selection tests (no .select),
pinning the full default projected shape for the variant union, the STI Bug
variant, and the MTI Feature variant.

De-raw the STI-create discriminator read-back to an ORM read with whole-shape
toEqual. Keep the MTI-create raw two-table check (base tasks row + features
variant row) as a storage-level invariant the ORM hides, with a why-comment;
keep raw DDL and seed inserts.
Extend poly-target include integration coverage in a new sibling file
polymorphism-include-relationships.test.ts with local standalone
fixtures (deep-cloned poly contracts + per-test DDL/seeds; shared
helpers.ts untouched). Whole-shape toEqual, base-id ordering.

Scenarios:
- poly (MTI) model as the include PARENT (correlation across base +
  variant tables)
- to-one (N:1) include whose TARGET is a poly model (per-row variant
  mapping on a single object)
- a base with two MTI variant tables (no cross-variant column
  contamination)
- relationship-level implicit-default selection for STI and MTI (no
  .select, full default per-variant shape — the rule's exception)

The nested-include-through-poly-target scenario surfaced a real decode
defect (grandchild stitches to null when the include target is poly):
landed as it.skip asserting the correct shape, with root-cause notes,
as a regression target. Not patched here (test-only scope).

Add a TML-2783 note to the existing select-leak MTI assertion so the
known poly-variant-column leak is traceable.
… the raw child row

A nested .include(...) hanging off a polymorphic include target decoded the
grandchild to an empty value for every row. The poly branch of
decodeIncludePayload mapped the child row via mapPolymorphicRow first — which
keeps only variant model-field columns and so drops the nested payload's
relation alias — then read each nested-include payload from the mapped row,
where it was already gone.

Source each nested-include payload from the raw child row instead, which always
carries the payload under its relation alias. Behavior-preserving for non-poly
includes; leaves mapPolymorphicRow's per-variant shaping untouched.

Adds a unit test covering an MTI and a non-MTI variant (asserting both the
stitched grandchild and that sibling-variant columns are still dropped) and
unskips the integration scenario.
@tensordreams tensordreams marked this pull request as ready for review June 1, 2026 17:04
@tensordreams tensordreams requested a review from a team as a code owner June 1, 2026 17:04
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