TML-2683: wire polymorphism into the SQL ORM .include() child path#669
Conversation
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.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…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.
size-limit report 📦
|
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/emitter
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
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.
…lden-rule pointer
…ge); rule implicit-default carve-out
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.
…epth-2 decode defect
… 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.
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-specificwherefilters 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)
bb42ab153)decodeIncludePayloadmaps poly child rows viamapPolymorphicRow. (20b963758).variant()narrowing + result type (bare include types as the variant union). (4c8f2d460)where. (34becbd8a)where: variant-aware predicate accessor. (d5d9204b4)polymorphism.test.ts: whole-shapetoEqual, STI/MTI implicit-default tests, de-raw. (abbaacd77)c41940f73)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-resulttoEqual+ explicitselect). Slice spec / plan / design-notes inprojects/tml-2683/.Follow-ups (filed; out of scope here)
orderByon a variant-narrowed collection drops MTI variant-field columns..select(...)on a poly include doesn't restrict variant-table columns (filed in May WS2).