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

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

coderabbitaiBot 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main fix for unnamed relations resolving to the wrong opposite field.
Linked Issues check✅ PassedThe code changes address #2757 by matching opposite relation names symmetrically and adding regression coverage.
Out of Scope Changes check✅ PassedThe schema update and regression test additions are directly tied to the relation-resolution fix and are not out of scope.
Docstring Coverage✅ PassedNo 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.

@coderabbitaicoderabbitaiBot 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 devJul 27, 2026
10 checks passed
@ymc9
ymc9 deleted the fix/issue-2757-unnamed-relation-opposite branch July 27, 2026 11:06
Sign up for freeto 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

@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

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

coderabbitaiBot 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main fix for unnamed relations resolving to the wrong opposite field.
Linked Issues check✅ PassedThe code changes address #2757 by matching opposite relation names symmetrically and adding regression coverage.
Out of Scope Changes check✅ PassedThe schema update and regression test additions are directly tied to the relation-resolution fix and are not out of scope.
Docstring Coverage✅ PassedNo 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.

@coderabbitaicoderabbitaiBot 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 devJul 27, 2026
10 checks passed
@ymc9
ymc9 deleted the fix/issue-2757-unnamed-relation-opposite branch July 27, 2026 11:06
Sign up for freeto 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

@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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

coderabbitaiBot 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main fix for unnamed relations resolving to the wrong opposite field.
Linked Issues check✅ PassedThe code changes address #2757 by matching opposite relation names symmetrically and adding regression coverage.
Out of Scope Changes check✅ PassedThe schema update and regression test additions are directly tied to the relation-resolution fix and are not out of scope.
Docstring Coverage✅ PassedNo 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.

@coderabbitaicoderabbitaiBot 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 devJul 27, 2026
10 checks passed
@ymc9
ymc9 deleted the fix/issue-2757-unnamed-relation-opposite branch July 27, 2026 11:06
Sign up for freeto 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

@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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

coderabbitaiBot 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main fix for unnamed relations resolving to the wrong opposite field.
Linked Issues check✅ PassedThe code changes address #2757 by matching opposite relation names symmetrically and adding regression coverage.
Out of Scope Changes check✅ PassedThe schema update and regression test additions are directly tied to the relation-resolution fix and are not out of scope.
Docstring Coverage✅ PassedNo 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.

@coderabbitaicoderabbitaiBot 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 devJul 27, 2026
10 checks passed
@ymc9
ymc9 deleted the fix/issue-2757-unnamed-relation-opposite branch July 27, 2026 11:06
Sign up for freeto 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

@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

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

coderabbitaiBot 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main fix for unnamed relations resolving to the wrong opposite field.
Linked Issues check✅ PassedThe code changes address #2757 by matching opposite relation names symmetrically and adding regression coverage.
Out of Scope Changes check✅ PassedThe schema update and regression test additions are directly tied to the relation-resolution fix and are not out of scope.
Docstring Coverage✅ PassedNo 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.

@coderabbitaicoderabbitaiBot 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 devJul 27, 2026
10 checks passed
@ymc9
ymc9 deleted the fix/issue-2757-unnamed-relation-opposite branch July 27, 2026 11:06
Sign up for freeto 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

@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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

coderabbitaiBot 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main fix for unnamed relations resolving to the wrong opposite field.
Linked Issues check✅ PassedThe code changes address #2757 by matching opposite relation names symmetrically and adding regression coverage.
Out of Scope Changes check✅ PassedThe schema update and regression test additions are directly tied to the relation-resolution fix and are not out of scope.
Docstring Coverage✅ PassedNo 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.

@coderabbitaicoderabbitaiBot 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 devJul 27, 2026
10 checks passed
@ymc9
ymc9 deleted the fix/issue-2757-unnamed-relation-opposite branch July 27, 2026 11:06
Sign up for freeto 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

@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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

coderabbitaiBot 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main fix for unnamed relations resolving to the wrong opposite field.
Linked Issues check✅ PassedThe code changes address #2757 by matching opposite relation names symmetrically and adding regression coverage.
Out of Scope Changes check✅ PassedThe schema update and regression test additions are directly tied to the relation-resolution fix and are not out of scope.
Docstring Coverage✅ PassedNo 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.

@coderabbitaicoderabbitaiBot 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 devJul 27, 2026
10 checks passed
@ymc9
ymc9 deleted the fix/issue-2757-unnamed-relation-opposite branch July 27, 2026 11:06
Sign up for freeto 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

@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

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

coderabbitaiBot 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main fix for unnamed relations resolving to the wrong opposite field.
Linked Issues check✅ PassedThe code changes address #2757 by matching opposite relation names symmetrically and adding regression coverage.
Out of Scope Changes check✅ PassedThe schema update and regression test additions are directly tied to the relation-resolution fix and are not out of scope.
Docstring Coverage✅ PassedNo 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.

@coderabbitaicoderabbitaiBot 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 devJul 27, 2026
10 checks passed
@ymc9
ymc9 deleted the fix/issue-2757-unnamed-relation-opposite branch July 27, 2026 11:06
Sign up for freeto 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

@ymc9