Skip to content

fix(sdk): unnamed relation resolving to the wrong opposite field - #2769

Merged
ymc9 merged 1 commit into
devfrom
fix/issue-2757-unnamed-relation-opposite
Jul 27, 2026
Merged

fix(sdk): unnamed relation resolving to the wrong opposite field#2769
ymc9 merged 1 commit into
devfrom
fix/issue-2757-unnamed-relation-opposite

Conversation

@ymc9

@ymc9 ymc9 commented Jul 27, 2026

Copy link
Copy Markdown
Member

Fixes #2757

Problem

When two relations connect the same pair of models and only one pair carries @relation("name"), the unnamed pair could resolve to the named pair's field. The generated schema then had e.g. Category.products.relation.opposite = "featuredIn", so relation filters (some/none/every) and nested reads traversed the wrong FK column and silently returned wrong results — no error, types still compile. Prisma resolves the same schema correctly.

model Category {
  id       String    @id @default(cuid())
  products Product[]                       // opposite should be Product.category
  featured Product[] @relation("Featured")
}

model Product {
  id           String    @id @default(cuid())
  featuredIn   Category? @relation("Featured", fields: [featuredInId], references: [id])
  featuredInId String?
  category     Category  @relation(fields: [categoryId], references: [id])
  categoryId   String
}

category.findMany({ where: { products: { none: {} } } }) joined on featuredInId instead of categoryId.

Cause

TsSchemaGenerator.getOppositeRelationField matched relation names asymmetrically — a named field required a name match, but an unnamed field returned the first back-pointing field regardless of its relation name. Resolution therefore depended on field declaration order, which is exactly the ordering sensitivity the reporter observed.

Fix

Require the relation name to match on both sides, including the case where neither side is named. This is the same rule the language validator already applies when pairing relation fields (datamodel-validator.ts), so the generator and validation now agree, and declaration order no longer matters. Schemas that name only one side are already rejected by validation, so nothing that previously validated changes meaning.

Nothing else computes the opposite field — the ORM runtime only reads relation.opposite from the generated schema.

Verification

  • New regression test tests/regression/test/issue-2757.test.ts covers both declaration orders, nested reads, and some/none filters on both sides. The "named relation declared first" case failed before this change.
  • Full regression suite: 154 files / 213 tests passed (18 pre-existing skips).
  • e2e ORM suite: 122 files / 1039 tests passed.
  • Regenerating the checked-in e2e schemas changed exactly one file, and it is a genuine instance of this bug in the wild: trigger.dev's Project.workerGroups resolved to defaultForProjects (the @relation("ProjectDefaultWorkerGroup") pair) and now correctly resolves to WorkerInstanceGroup.project, the field carrying projectId.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Corrected relationship mapping when named and unnamed relationships are used together.
    • Fixed generated relationship metadata so bidirectional links resolve to the correct fields.
    • Nested reads and relationship filters now return accurate results for models with multiple relationships.
  • Tests

    • Added regression coverage for relationship traversal, filtering, and nested data retrieval.

When two relations connect the same pair of models and only one pair
carries `@relation("name")`, the unnamed pair could resolve to the named
pair's field. The generated schema then had e.g.
`Category.products.relation.opposite = "featuredIn"`, so relation filters
and nested reads traversed the wrong FK column and silently returned
wrong results.

`getOppositeRelationField` matched relation names asymmetrically: a named
field required a name match, but an unnamed field accepted the first
back-pointing field regardless of its name, making resolution depend on
field declaration order. Match names on both sides, including the case
where neither side is named - the same rule the language validator
already applies.

Regenerating the e2e schemas surfaces one real-world instance of this:
trigger.dev's `Project.workerGroups` now correctly resolves to
`WorkerInstanceGroup.project` instead of the `ProjectDefaultWorkerGroup`
relation's `defaultForProjects`.

Fixes #2757

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The schema generator now requires symmetric relation-name matches when selecting opposite fields. The generated schema fixture is updated, and regression tests cover unnamed and named relations through nested reads and relation filters.

Changes

Relation resolution

Layer / File(s) Summary
Symmetric relation-name matching
packages/sdk/src/ts-schema-generator.ts
getOppositeRelationField now matches opposite fields only when their relation names are equal, including unnamed pairs.
Generated mapping and regression coverage
tests/e2e/github-repos/trigger.dev/schema.ts, tests/regression/test/issue-2757.test.ts
The generated Project.workerGroups mapping is corrected, and tests validate nested reads and some/none relation filters for named and unnamed relations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix for unnamed relations resolving to the wrong opposite field.
Linked Issues check ✅ Passed The code changes address #2757 by matching opposite relation names symmetrically and adding regression coverage.
Out of Scope Changes check ✅ Passed The schema update and regression test additions are directly tied to the relation-resolution fix and are not out of scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 fix/issue-2757-unnamed-relation-opposite

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.

@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: 1

🤖 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 `@tests/regression/test/issue-2757.test.ts`:
- Around line 46-58: Extend the relation-filter regression coverage in
issue-2757 by seeding a second product with opposing category and featuredIn
values, then add category.findMany assertions for products.every and
featured.every. Ensure the expected category results validate both
every-relation paths alongside the existing some and none cases.
🪄 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.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d2dee002-59eb-4269-a9de-9c0e3e92763f

📥 Commits

Reviewing files that changed from the base of the PR and between ef0db7f and e01bdf1.

📒 Files selected for processing (3)
  • packages/sdk/src/ts-schema-generator.ts
  • tests/e2e/github-repos/trigger.dev/schema.ts
  • tests/regression/test/issue-2757.test.ts

Comment on lines +46 to +58
// relation filters traverse the right column
await expect(db.category.findMany({ where: { products: { none: {} } } })).resolves.toEqual([
expect.objectContaining({ id: c2.id }),
]);
await expect(db.category.findMany({ where: { products: { some: {} } } })).resolves.toEqual([
expect.objectContaining({ id: c1.id }),
]);
await expect(db.category.findMany({ where: { featured: { none: {} } } })).resolves.toEqual([
expect.objectContaining({ id: c1.id }),
]);
await expect(db.category.findMany({ where: { featured: { some: {} } } })).resolves.toEqual([
expect.objectContaining({ id: c2.id }),
]);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cover the every relation-filter path.

The stated regression scope includes every, but these assertions only exercise some and none. Seed a second product with opposing category/featuredIn values and assert every for both relations; otherwise a regression specific to every can ship undetected.

🤖 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 `@tests/regression/test/issue-2757.test.ts` around lines 46 - 58, Extend the
relation-filter regression coverage in issue-2757 by seeding a second product
with opposing category and featuredIn values, then add category.findMany
assertions for products.every and featured.every. Ensure the expected category
results validate both every-relation paths alongside the existing some and none
cases.

@ymc9
ymc9 merged commit fdf5616 into dev Jul 27, 2026
10 checks passed
@ymc9
ymc9 deleted the fix/issue-2757-unnamed-relation-opposite branch July 27, 2026 11:06
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.

Multiple FK to same entity cause issue with an unnamed relation and values not returned

1 participant