Skip to content

TML-2784: M:N slice 0 — contract + resolver foundation#678

Open
tensordreams wants to merge 16 commits into
mainfrom
tml-2784-slice-0-contract-resolver-foundation
Open

TML-2784: M:N slice 0 — contract + resolver foundation#678
tensordreams wants to merge 16 commits into
mainfrom
tml-2784-slice-0-contract-resolver-foundation

Conversation

@tensordreams
Copy link
Copy Markdown
Contributor

@tensordreams tensordreams commented Jun 1, 2026

Slice 0 of the SQL ORM: Many-to-Many End to End project (Linear project). The foundation that gates the parallel read/filter/write slices.

Stacked PR. Base is tml-2597 (#673, project spec/plan) → tml-2729 (#667) → main. Review/merge the stack bottom-up.

Overview

Many-to-many relations are authored + lowered correctly, but the runtime stack could not even validate an emitted M:N contract: the relation validator rejected cardinality: N:M (not in its enum) and the through key (undeclared on a reject-policy object). This slice makes the M:N relation shape first-class through the contract and surfaces a uniform through descriptor on the shared ORM resolver — without yet teaching any consumer to walk it (that is slices 1–3).

Changes

  • Contract shape (f962fd47d): N:M added to the relation cardinality enum and an optional through { table, parentColumns[], childColumns[], targetColumns[] } across the arktype validator, the JSON schema, and the ContractReferenceRelation type. Deleted the as ContractRelation[cardinality] cast (the "until the contract type is extended for many-to-many" seam), reconciled the emitted parentCols/childColsparentColumns/childColumns drift, and populated targetColumns. Round-trip test added (authors rel.manyToMany → emits → validateContract passes — failed before).
  • Cardinality canonicalisation (f255714e7): flipped the four M:N sites in sql-orm-client to N:M (the contract/PSL/lowering already used N:M; the ORM client was the lone holdout, so a real emitted M:N parsed to undefined). The M:N-rejection unit test moved to N:M so it exercises the live guard branch (still a rejection — slice 3 flips it positive).
  • Resolver through (3a87c7c55, 5ff87bc21): ResolvedRelation.through on the single shared resolveModelRelations resolver, including requiredPayloadColumns (junction columns that are NOT NULL, no default, and not FK columns — slice 3 uses this to disable nested .create on required-payload junctions). 4-case resolver test (simple / composite-key / required-payload / all-nullable-or-defaulted).

Why

The relation-shaped ORM API over M:N is the most-asked-about migration gap from Prisma. This slice is the prerequisite the other three build on: there is exactly one shared resolver (resolveModelRelationsResolvedRelation) feeding includes, filters, and mutations, so surfacing through once is the uniform primitive each consumer will branch on. Discovered while shaping: the change is purely additive — existing non-M:N contracts emit byte-identically (zero golden drift), so no consumer breaks and no deprecation window is needed.

Scope

Foundation only. This PR does not teach the include / filter / write paths to walk through — those are slices 1 (TML-2785), 2 (TML-2786), 3 (TML-2787). No user-observable behaviour change yet, so no manual-QA script (the runtime M:N surfaces arrive in the consumer slices).

Verification

  • validateContract round-trips an M:N contract (was failing); ResolvedRelation.through populated incl. requiredPayloadColumns; no M:N literal remains in sql-orm-client/src.
  • sql-orm-client typecheck + 489 tests green; sql-contract build + downstream typecheck green; pnpm lint:deps clean; pnpm lint:casts delta=-1; golden fixtures diff empty (additive). fixtures:check runs in CI (the local emit step has a pre-existing CLI-on-PATH env limitation in this sandbox; additivity confirmed via direct golden diff).

Refs: TML-2784.

Summary by CodeRabbit

  • New Features

    • Explicit support for many-to-many (N:M) relations with optional join-table ("through") metadata (table, namespace, parent/child/target columns).
  • Updates

    • Standardized the many-to-many cardinality token to "N:M" across the system.
    • Validation, contract generation, and runtime relation resolution now recognize, preserve, and materialize through descriptors.
  • Tests

    • Added and updated tests covering N:M emission, validation, lowering, and runtime resolution.

@tensordreams tensordreams requested a review from a team as a code owner June 1, 2026 17:21
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

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 16 minutes and 51 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: c650b444-12b3-4cfd-9a56-58a2a62641f7

📥 Commits

Reviewing files that changed from the base of the PR and between 3a740c0 and 1479309.

⛔ Files ignored due to path filters (4)
  • packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.json is excluded by !**/generated/**
  • test/integration/test/sql-orm-client/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • test/integration/test/sql-orm-client/fixtures/generated/contract.json is excluded by !**/generated/**
📒 Files selected for processing (1)
  • test/integration/test/sql-orm-client/fixtures/contract.ts
📝 Walkthrough

Walkthrough

Adds ContractRelationThrough for junction-table metadata, emits through in generated types when present, updates validation and JSON schema to accept through for N:M, updates contract builder to populate through.namespaceId/targetColumns, implements resolveThrough with requiredPayloadColumns, and normalizes cardinality literals to 'N:M'.

Changes

Many-to-Many Relations with Junction Table Metadata (N:M)

Layer / File(s) Summary
Foundation contract types for N:M with through metadata
packages/1-framework/0-foundation/contract/src/domain-types.ts, packages/1-framework/0-foundation/contract/src/exports/types.ts
ContractRelationThrough type introduced with table and column lists; ContractReferenceRelation split into ContractManyToManyRelation (requires through) and ContractNonJunctionRelation (through?: never); re-export added.
Validation schemas for N:M relations
packages/2-sql/1-core/contract/src/validators.ts, packages/2-sql/2-authoring/contract-ts/schemas/data-contract-sql-v1.json, packages/2-sql/1-core/contract/test/validators.test.ts
Added ContractRelationThroughSchema and extended ModelRelation/ContractReferenceRelation to allow required through for N:M relations; JSON schema adds through with required fields and additionalProperties: false; tests cover accept/reject rules.
Type generation for N:M through metadata
packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts, packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts
Generated relation types now emit a readonly through object (table, namespaceId, readonly column tuples) for N:M; tests verify emission for composite keys and omission for non-N:M.
SQL contract building with through metadata
packages/2-sql/2-authoring/contract-ts/src/build-contract.ts, packages/2-sql/2-authoring/contract-ts/test/contract-builder.dsl.test.ts, packages/2-sql/2-authoring/contract-ts/test/contract.edge-cases.test.ts, packages/2-sql/2-authoring/contract-ts/test/contract-builder.contract-definition.test.ts
Contract builder builds tableNamespaceByName, sets cardinality from input, constructs ContractRelationThrough with namespaceId, parentColumns, childColumns, and computed targetColumns (primary key fallback → unique constraint → throw); tests validate lowering, targetColumns derivation, and edge cases.
Runtime relation resolution with through computation
packages/3-extensions/sql-orm-client/src/collection-contract.ts, packages/3-extensions/sql-orm-client/src/collection-internal-types.ts, packages/3-extensions/sql-orm-client/src/types.ts, packages/3-extensions/sql-orm-client/test/collection-contract.test.ts, packages/3-extensions/sql-orm-client/test/mutation-executor.test.ts
Adds ResolvedThrough with requiredPayloadColumns, implements resolveThrough(...) to validate and resolve junction tables, compute FK sets, and derive requiredPayloadColumns; wires into resolveModelRelations; normalizes cardinality to 'N:M' and updates dependent guards/tests.
Cardinality normalization M:N → N:M
packages/2-mongo-family/5-query-builders/orm/src/collection-state.ts, packages/3-extensions/sql-orm-client/...
Replaced 'M:N' with canonical 'N:M' in include expressions, internal conditional types, parser/guards, and tests; updated error/expectation text accordingly.
Integration fixtures
test/integration/test/sql-orm-client/fixtures/contract.ts
Adds Tag.label and new models UserTag, Role, UserRole, Project, ProjectLink; wires User.tags and User.roles many-to-many relations using through descriptors and registers models in base contract.

Sequence Diagram

sequenceDiagram
  participant resolveModelRelations
  participant resolveThrough
  participant contractStorage
  resolveModelRelations->>resolveThrough: pass raw through object
  resolveThrough->>contractStorage: lookup junction table by (namespaceId, table)
  contractStorage-->>resolveThrough: return junction table columns & metadata
  resolveThrough->>resolveThrough: compute FK column set (parentColumns + childColumns)
  resolveThrough->>resolveThrough: filter columns → requiredPayloadColumns (non-FK, NOT NULL, no-default)
  resolveThrough-->>resolveModelRelations: return ResolvedThrough
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • aqrln

🐰 A junction table joins the way,
N:M relations here to stay!
Through metadata flows with grace and care,
Many-to-many mapped everywhere! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% 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 'TML-2784: M:N slice 0 — contract + resolver foundation' clearly and specifically describes the main change: introducing many-to-many (M:N/N:M) relation support to the contract and resolver foundation.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2784-slice-0-contract-resolver-foundation

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.

@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@678

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 1479309

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

size-limit report 📦

Path Size
postgres / no-emit 145.41 KB (+0.25% 🔺)
postgres / emit 117.24 KB (+0.15% 🔺)
mongo / no-emit 76.04 KB (0%)
mongo / emit 70.89 KB (0%)
cf-worker / no-emit 174.87 KB (+0.14% 🔺)
cf-worker / emit 143.55 KB (+0.06% 🔺)

Comment thread packages/1-framework/0-foundation/contract/src/domain-types.ts
Comment thread packages/3-extensions/sql-orm-client/src/collection-contract.ts Outdated
@tensordreams tensordreams force-pushed the tml-2597-sql-orm-complete-end-to-end-many-to-many-support-include branch from 90cf526 to 669c230 Compare June 2, 2026 13:07
@tensordreams tensordreams force-pushed the tml-2784-slice-0-contract-resolver-foundation branch 2 times, most recently from e44f8aa to aaefe75 Compare June 3, 2026 08:51
@tensordreams tensordreams force-pushed the tml-2597-sql-orm-complete-end-to-end-many-to-many-support-include branch 2 times, most recently from 433e9bb to c350460 Compare June 3, 2026 11:32
@tensordreams tensordreams force-pushed the tml-2784-slice-0-contract-resolver-foundation branch from aaefe75 to 87de795 Compare June 3, 2026 11:32
Base automatically changed from tml-2597-sql-orm-complete-end-to-end-many-to-many-support-include to main June 3, 2026 11:37
@tensordreams tensordreams force-pushed the tml-2784-slice-0-contract-resolver-foundation branch from 87de795 to 4763690 Compare June 4, 2026 15:12
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts (1)

385-395: ⚡ Quick win

Add a regression case for non-N:M relations that still carry through.

Current coverage only checks omission when through is absent. Add a case where cardinality is N:1 (or 1:N) with a through object and assert readonly through: is not emitted.

🧪 Suggested test addition
+  it('omits through when relation is not N:M even if through is present', () => {
+    const result = generateModelRelationsType({
+      author: {
+        to: crossRef('User'),
+        cardinality: 'N:1',
+        through: {
+          table: 'user_author',
+          parentColumns: ['authorId'],
+          childColumns: ['userId'],
+          targetColumns: ['id'],
+        },
+      },
+    });
+    expect(result).not.toContain('readonly through:');
+  });
🤖 Prompt for 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.

In `@packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts`
around lines 385 - 395, Add a regression test that verifies
generateModelRelationsType does not emit a "readonly through:" property for
non-N:M relations even when a through object is present: update the test suite
around domain-type-generation.test.ts by adding a case that calls
generateModelRelationsType with a relation (e.g., author) whose cardinality is
'N:1' (or '1:N') but includes a through object, then assert the returned string
does not contain 'readonly through:' and still contains the expected 'readonly
localFields:' token; reference generateModelRelationsType and the existing test
pattern to mirror assertions and test structure.
🤖 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/collection-contract.ts`:
- Around line 257-295: The resolveThrough function currently proceeds even when
resolveTableForContract(contract, table) returns undefined, making
misspelled/stale junction tables appear valid; change resolveThrough so that if
resolvedJunction is undefined it returns undefined (i.e., reject the relation)
instead of continuing to build and returning a through object with
requiredPayloadColumns: []; locate resolveThrough and the call to
resolveTableForContract(contract, table) and add an early return undefined when
resolvedJunction is falsy before computing requiredPayloadColumns or returning
the through shape.

---

Nitpick comments:
In `@packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts`:
- Around line 385-395: Add a regression test that verifies
generateModelRelationsType does not emit a "readonly through:" property for
non-N:M relations even when a through object is present: update the test suite
around domain-type-generation.test.ts by adding a case that calls
generateModelRelationsType with a relation (e.g., author) whose cardinality is
'N:1' (or '1:N') but includes a through object, then assert the returned string
does not contain 'readonly through:' and still contains the expected 'readonly
localFields:' token; reference generateModelRelationsType and the existing test
pattern to mirror assertions and test structure.
🪄 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: c505dd74-597c-4364-9cc0-988eeb557c98

📥 Commits

Reviewing files that changed from the base of the PR and between c159cd4 and 4763690.

⛔ Files ignored due to path filters (8)
  • projects/sql-orm-many-to-many/learnings.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/00-contract-resolver-foundation/dispatches/01-contract-shape.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/00-contract-resolver-foundation/dispatches/02-cardinality-canonicalise.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/00-contract-resolver-foundation/dispatches/03-resolver-through.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/00-contract-resolver-foundation/dispatches/03-resolver-through.r2.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/00-contract-resolver-foundation/plan.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/slices/00-contract-resolver-foundation/spec.md is excluded by !projects/**
  • projects/sql-orm-many-to-many/trace.jsonl is excluded by !projects/**
📒 Files selected for processing (16)
  • packages/1-framework/0-foundation/contract/src/domain-types.ts
  • packages/1-framework/0-foundation/contract/src/exports/types.ts
  • packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts
  • packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts
  • packages/2-mongo-family/5-query-builders/orm/src/collection-state.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/2-authoring/contract-ts/schemas/data-contract-sql-v1.json
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.dsl.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.edge-cases.test.ts
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/3-extensions/sql-orm-client/src/collection-internal-types.ts
  • packages/3-extensions/sql-orm-client/src/mutation-executor.ts
  • packages/3-extensions/sql-orm-client/src/types.ts
  • packages/3-extensions/sql-orm-client/test/collection-contract.test.ts
  • packages/3-extensions/sql-orm-client/test/mutation-executor.test.ts

Comment thread packages/3-extensions/sql-orm-client/src/collection-contract.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/3-extensions/sql-orm-client/src/collection-contract.ts (1)

283-300: ⚠️ Potential issue | 🟠 Major

Missing junction table still returns a valid-looking through descriptor.

When contract.storage.namespaces[namespaceId]?.tables[table] returns undefined, the function proceeds and returns a ResolvedThrough object with requiredPayloadColumns: []. This masks invalid junction table names (typos, wrong namespace, or stale metadata) and defers the error to downstream SQL generation rather than failing fast at resolution time.

This mirrors the concern raised in the previous review comment on lines 255-300. Consider returning undefined when junctionTable is not found so that invalid through metadata is detected early:

💡 Suggested early-return guard
 const junctionTable = contract.storage.namespaces[namespaceId]?.tables[table];
+if (!junctionTable) {
+  return undefined;
+}
+
 const requiredPayloadColumns: string[] = [];
-if (junctionTable) {
-  for (const [colName, col] of Object.entries(junctionTable.columns)) {
-    if (!fkColumnSet.has(colName) && !col.nullable && col.default === undefined) {
-      requiredPayloadColumns.push(colName);
-    }
+for (const [colName, col] of Object.entries(junctionTable.columns)) {
+  if (!fkColumnSet.has(colName) && !col.nullable && col.default === undefined) {
+    requiredPayloadColumns.push(colName);
   }
 }
🤖 Prompt for 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.

In `@packages/3-extensions/sql-orm-client/src/collection-contract.ts` around lines
283 - 300, The current resolution builds a ResolvedThrough even when
junctionTable is undefined (lookup via
contract.storage.namespaces[namespaceId]?.tables[table]), which hides invalid
through metadata; change the logic in the function that computes junctionTable
so that if contract.storage.namespaces[namespaceId]?.tables[table] is undefined
you return undefined immediately (instead of continuing and returning a
ResolvedThrough with requiredPayloadColumns: []), ensuring consumers see a
missing-through early; update any callers/type annotations if necessary to
accept undefined from the ResolvedThrough resolver.
🤖 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.

Duplicate comments:
In `@packages/3-extensions/sql-orm-client/src/collection-contract.ts`:
- Around line 283-300: The current resolution builds a ResolvedThrough even when
junctionTable is undefined (lookup via
contract.storage.namespaces[namespaceId]?.tables[table]), which hides invalid
through metadata; change the logic in the function that computes junctionTable
so that if contract.storage.namespaces[namespaceId]?.tables[table] is undefined
you return undefined immediately (instead of continuing and returning a
ResolvedThrough with requiredPayloadColumns: []), ensuring consumers see a
missing-through early; update any callers/type annotations if necessary to
accept undefined from the ResolvedThrough resolver.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 77e8d72f-6bef-4549-b0de-8f193236eac6

📥 Commits

Reviewing files that changed from the base of the PR and between 4763690 and d15ad0b.

📒 Files selected for processing (8)
  • packages/1-framework/0-foundation/contract/src/domain-types.ts
  • packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts
  • packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/2-authoring/contract-ts/schemas/data-contract-sql-v1.json
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/3-extensions/sql-orm-client/test/collection-contract.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts
  • packages/3-extensions/sql-orm-client/test/collection-contract.test.ts
  • packages/1-framework/0-foundation/contract/src/domain-types.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts

Contract + resolver foundation: make M:N a validatable contract shape
(through + N:M), surface ResolvedRelation.through, canonicalise on N:M.
3 dispatches (contract substrate / N:M rename / resolver judgment).

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

Extend ContractReferenceRelation to accept cardinality 'N:M' and an
optional through: { table, parentColumns, childColumns, targetColumns }.
Three places updated in lockstep:

- domain-types.ts: add 'N:M' to cardinality union, add ContractRelationThrough
  type and optional through field
- validators.ts: add 'N:M' to ContractReferenceRelationSchema enum, add
  ContractRelationThroughSchema with its own '+': 'reject' guard
- data-contract-sql-v1.json: add through object to ModelRelation def
- build-contract.ts: remove the 'as ContractRelation['cardinality']' cast
  (now unnecessary), rename emitted parentCols/childCols to parentColumns/
  childColumns, populate targetColumns from the target model's PK
- mongo-orm collection-state.ts: align local cardinality type from 'M:N' to
  'N:M' (trivial consumer fix required by the type widening)

New round-trip test: author User-UserTag-Tag M:N via rel.manyToMany,
emit, validateSqlContractFully passes, and through descriptor is asserted.

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

The SQL contract already emits 'N:M' (landed in D1). sql-orm-client was the
lone holdout still spelling it 'M:N', meaning a real emitted M:N relation
parsed to undefined and the mutation guard silently never fired.

Flip all four literal sites in src/:
- RelationCardinalityTag union (types.ts)
- partitionByOwnership guard + error message (mutation-executor.ts)
- IsToManyRelation type-level check (collection-internal-types.ts)
- parseRelationCardinality acceptance check (collection-contract.ts)

Update tests to match: move the rejection test's cardinality literal to
'N:M' (stays a rejection test), update the guard error message regex,
and update the isToOneCardinality argument in collection-contract.test.ts
(required for typecheck: 'M:N' is no longer assignable to
RelationCardinalityTag).

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

Add ResolvedThrough interface and populate through on ResolvedRelation when
the contract relation carries a through object (cardinality N:M). The resolver
reads table, parentColumns, childColumns, and targetColumns directly from the
contract, then derives requiredPayloadColumns by inspecting the junction
table's storage columns: any column that is NOT NULL, has no default, and is
not in parentColumns ∪ childColumns is required payload that the caller must
supply when creating a junction row.

Four resolver unit test cases: simple single-column FK, composite-key junction,
junction with required non-FK payload columns, and junction with only
nullable/defaulted extra columns.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The 5 bare `as` casts in `resolveThrough` (parentColumns/childColumns in
the Set spread, and all three *Columns in the return object) were
declarative widenings from `unknown[]` to `readonly string[]`.  Replace
all five with `castAs<readonly string[]>(…)` from `@prisma-next/utils/casts`
per the no-bare-casts rule.

No behaviour change.  lint:casts delta: -1.

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

Orchestrator artifacts for the slice-0 build loop (3 dispatches, D3 in 2
rounds). Review log under reviews/ is gitignored by repo convention.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
Teach generateModelRelationsType to serialize the junction-table
descriptor (through.table / parentColumns / childColumns /
targetColumns) as a typed readonly literal when present in the relation
object.  Non-M:N relations (no through key) are byte-for-byte unchanged.

Adds three unit tests: M:N with single-column keys, M:N with a
composite parent key, and a regression guard that confirms through is
absent on a plain N:1 relation that carries only an on block.

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

Main's namespace work (TML-2605) removed the unboundTable helper the
resolver relied on and made runtime SQL namespace-qualified. Resolve the
junction via resolveTableForContract so requiredPayloadColumns reads the
correct table and the resolved namespaceId is carried on the descriptor.

Consolidate ResolvedThrough onto ContractRelationThrough (export the
latter from @prisma-next/contract) and surface the junction namespaceId,
addressing Will's review on #678 (collection-contract.ts:201
'Needs namespace' + 'Duplicate of ContractRelationThrough?').

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

Nothing constrains the source, target, and junction tables of an N:M
relation to one namespace, so resolving the junction by bare table name
is ambiguous under cross-namespace collisions (TML-2550). Mirror the
FK-reference shape: the emitted through block now carries the junction's
namespaceId (JSON schema + arktype validator + ContractRelationThrough +
build-contract emission + contract.d.ts literal), and the orm-client
resolver reads it from the contract — looking the junction up in its
declared namespace — instead of re-deriving by name scan.

Follows up wmadden's review on #678 (domain-types.ts 'Needs namespace').

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
@tensordreams tensordreams force-pushed the tml-2784-slice-0-contract-resolver-foundation branch from d15ad0b to 5450121 Compare June 5, 2026 12:44
Comment thread packages/1-framework/0-foundation/contract/src/domain-types.ts Outdated
Comment thread packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts Outdated
Comment thread packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts Outdated
Comment thread packages/2-sql/1-core/contract/src/validators.ts Outdated
Comment thread packages/2-sql/2-authoring/contract-ts/src/build-contract.ts Outdated
Comment thread packages/2-sql/2-authoring/contract-ts/src/build-contract.ts Outdated
Comment thread packages/3-extensions/sql-orm-client/src/collection-contract.ts Outdated
Comment thread packages/3-extensions/sql-orm-client/src/collection-contract.ts Outdated
Comment thread packages/3-extensions/sql-orm-client/test/collection-contract.test.ts Outdated
Comment thread packages/3-extensions/sql-orm-client/test/collection-contract.test.ts Outdated
…s N:M

Split ContractReferenceRelation into a discriminated union: an N:M
variant that requires through, and a non-junction variant that forbids
it (through?: never). Mirror the invariant in the arktype validator with
a discriminated schema and drop the all-optional through cast plus the
per-field presence guard in the emitter, which branches on through
presence only now that a present through always carries all five fields.

Addresses PR #678 review threads A01-A04.

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

Replace the silent fallbacks in M:N through lowering: throw when the
junction table is not a declared model (instead of defaulting its
namespace), and derive junction targetColumns from the target's primary
id, else its first unique constraint, else throw (instead of an empty
array).

Addresses PR #678 review threads A05, A06.

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

Accept ContractRelationThrough | undefined (the validateContract-validated
shape) instead of an opaque all-unknown bag, dropping the redundant
typeof/Array.isArray re-validation and the castAs<readonly string[]>
casts it forced. resolveThrough now returns undefined when the junction
table is absent from its declared namespace, so a stale/misspelled
junction fails fast instead of masquerading as a valid descriptor.

Addresses PR #678 review threads A07, A08, A09.

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

Replace the hand-rolled buildManyToManyContract() blob (returned via
'as unknown as Contract<SqlStorage>') with a fixture patched onto the
generated base contract and run through the Postgres serializer's
deserializeContract, so the through-descriptor suite exercises a
genuinely validated contract. Add a regression test asserting
resolveThrough omits through when the junction storage table is absent.

Addresses PR #678 review threads A07, A11, A12.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
@tensordreams
Copy link
Copy Markdown
Contributor Author

Re: the nitpick to add a regression test asserting generateModelRelationsType omits through for a non-N:M relation that carries a through object — this is now superseded by the type/validator tightening landed in a4e4297. ContractReferenceRelation is a discriminated union where the non-junction variant has through?: never, and the arktype validator rejects through on non-N:M relations, so 'non-N:M with through' is an unrepresentable contract state. A regression test would have to construct an impossible input; the emitter's behaviour is now guaranteed by the type rather than a runtime branch worth locking in.

@tensordreams
Copy link
Copy Markdown
Contributor Author

The duplicate finding here (don't return a resolved through when the junction table can't be found) is addressed: resolveThrough now returns undefined when the junction table is absent from its declared namespace, with a regression test. Landed in 6be6105 (test in 1e05792).

Comment thread packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts Outdated
… cast

generateModelRelationsType blind-cast relObj['through'] with the
invariant 'through is present iff cardinality is N:M' asserted in a
justification string. Gate the through emission on cardinality === 'N:M'
and read through off a single castAs<ContractManyToManyRelation> at the
JSON union boundary, so the discriminated union proves the shape.

Emitter output is byte-identical for valid contracts.

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

Add genuine M:N surface to the shared sql-orm-client test fixture so the
through-descriptor tests run against an emitted, validated contract
instead of a hand-patched one:

- User.tags -> Tag through UserTag (user_tags), single-column FK pair;
  user_tags also carries a nullable note and a now()-defaulted created_at
  to cover the requiredPayloadColumns exclusion path.
- User.roles -> Role through UserRole (user_roles) with a NOT NULL
  no-default level column, covering requiredPayloadColumns = ['level'].
- Project.related -> Project through ProjectLink (project_links) over
  composite (tenant_id,id) keys, covering composite parent/child/target
  column arrays.

Rewrite the through-descriptor tests to read these relations from the
emitted fixture via getTestContract(), and delete buildManyToManyContract.
The missing-junction case stays as a targeted withPatchedDomainModels
mutation (through.table -> bogus name) on the emitted contract.

Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
The integration schema creates tags(id, name); a contract-declared label
column the table doesn't have breaks implicit selection in the stacked
include tests. No slice-0 test reads it.

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.

3 participants