Skip to content

fix(core): Model#equals always returns true for instances of different models#18143

Merged
WikiRik merged 9 commits intosequelize:mainfrom
maksimf:fix/model-equals-cross-model-comparison
Mar 8, 2026
Merged

fix(core): Model#equals always returns true for instances of different models#18143
WikiRik merged 9 commits intosequelize:mainfrom
maksimf:fix/model-equals-cross-model-comparison

Conversation

@maksimf
Copy link
Contributor

@maksimf maksimf commented Mar 7, 2026

Summary

Fixes #18142.

  • Model#equals was comparing this.modelDefinition against itself (copy-paste bug on line 4518), so the model-type guard was always a no-op
  • Two instances of completely different models with the same primary key would incorrectly be considered equal
  • Fix: change const otherModelDefinition = this.modelDefinitionconst otherModelDefinition = other.modelDefinition
  • Adds a dedicated unit test suite for Model#equals covering same-model, cross-model, and non-Model comparisons

Regression note

This is a recurring regression in equals() — the same class of bug was previously reported and fixed in:

The current regression was introduced when the codebase was refactored to use modelDefinition.

Test plan

  • New unit test packages/core/test/unit/instance/equals.test.js covers:
    • Same instance compared to itself → true
    • Two instances of the same model with identical PK → true
    • Two instances of the same model with different PKs → false
    • Instances of different models with the same PK → false (was broken)
    • Comparing to null, undefined, plain object → false

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed instance equality logic so comparisons use the other instance’s model data, ensuring distinct models and records are correctly distinguished and preventing incorrect matches across model types or identical-looking records.
  • Tests

    • Added unit tests covering equality cases: same instance, different instances with same/different primary keys, cross-model comparisons, and comparisons against non-model values to prevent regressions.

…t models

otherModelDefinition was incorrectly assigned from this.modelDefinition
instead of other.modelDefinition, making the model-type guard a no-op.
Adds a unit test covering cross-model comparison.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@maksimf maksimf requested a review from a team as a code owner March 7, 2026 12:43
@maksimf maksimf requested review from WikiRik and sdepold March 7, 2026 12:43
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Fixed a copy‑paste bug in Model#equals: the method now compares other.modelDefinition (not this.modelDefinition) so cross‑model comparisons correctly return false; added unit tests covering self, same‑model, cross‑model, and non‑Model comparisons.

Changes

Cohort / File(s) Summary
Core logic
packages/core/src/model.js
Replace incorrect assignment of otherModelDefinition = this.modelDefinition with other.modelDefinition to enable the cross‑model mismatch guard.
Unit tests
packages/core/test/unit/instance/equals.test.ts
Add tests for Model#equals(): self comparison, same‑model equal/different PKs, cross‑model same PK, and comparisons against non‑Model values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through lines of code at night,
Found a twin that looked just right.
I nudged “other” into its place,
Now equals sees each model's face. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the bug fix: Model#equals returning true for different models when comparing instances with the same PK.
Linked Issues check ✅ Passed The PR fully addresses all objectives from #18142: fixes the copy-paste bug by reassigning otherModelDefinition to other.modelDefinition, adds comprehensive unit tests covering all required scenarios.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the Model#equals bug and adding tests; no unrelated modifications are present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Member

@WikiRik WikiRik left a comment

Choose a reason for hiding this comment

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

Good catch!

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/core/test/unit/instance/equals.test.ts (1)

8-14: Declare id attribute for TypeScript type safety.

The models don't declare the id attribute, but it's used in build() calls throughout the tests. While Sequelize adds id as the default primary key at runtime, InferAttributes only picks up explicitly declared properties, so TypeScript won't recognize id in the build options.

♻️ Suggested fix
-    class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
-      declare username: string;
-    }
-
-    class Project extends Model<InferAttributes<Project>, InferCreationAttributes<Project>> {
-      declare title: string;
-    }
+    class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
+      declare id: number;
+      declare username: string;
+    }
+
+    class Project extends Model<InferAttributes<Project>, InferCreationAttributes<Project>> {
+      declare id: number;
+      declare title: string;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/test/unit/instance/equals.test.ts` around lines 8 - 14, The
tests use build() with an id property but the Model subclasses (User and
Project) don't declare id, so TypeScript won't recognize it; add an explicit id
declaration (e.g. declare id?: number) to both class User and class Project
declarations so InferAttributes includes id for build/create options and other
usages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/core/test/unit/instance/equals.test.ts`:
- Around line 8-14: The tests use build() with an id property but the Model
subclasses (User and Project) don't declare id, so TypeScript won't recognize
it; add an explicit id declaration (e.g. declare id?: number) to both class User
and class Project declarations so InferAttributes includes id for build/create
options and other usages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da004d41-58aa-4b28-a11b-f4a05fad6fdb

📥 Commits

Reviewing files that changed from the base of the PR and between 69517b8 and a69f5ce.

📒 Files selected for processing (1)
  • packages/core/test/unit/instance/equals.test.ts

Co-authored-by: Rik Smale <13023439+WikiRik@users.noreply.github.com>
@WikiRik WikiRik enabled auto-merge (squash) March 7, 2026 13:13
@WikiRik WikiRik self-requested a review March 7, 2026 13:59
@WikiRik
Copy link
Member

WikiRik commented Mar 7, 2026

I approved too early, can you fix the CI issues? For the failing test; Model#equals is used in the following snippet

const alreadyAssociated =
!oldInstance || !associatedInstanceOrPk
? false
: associatedInstanceOrPk instanceof Model
? associatedInstanceOrPk.equals(oldInstance)
: // @ts-expect-error -- TODO: what if the target has no primary key?
oldInstance.get(this.target.primaryKeyAttribute) === associatedInstanceOrPk;

That also seems to be the only place that we use it internally. Model#equalsOneOf isn't used

claude and others added 2 commits March 7, 2026 14:20
Cast to the base Model type at the cross-model call site so TypeScript
accepts it without changing the equals(other: this) declaration:
  (user as Model).equals(project)
Also cast null/undefined with as unknown as Model in the non-Model test,
consistent with the existing plain-object cast.

https://claude.ai/code/session_01DqaGiyPC1Eo1VXDVdwJ3cG
auto-merge was automatically disabled March 7, 2026 14:24

Head branch was pushed to by a user without write access

@maksimf
Copy link
Contributor Author

maksimf commented Mar 7, 2026

I approved too early, can you fix the CI issues? For the failing test; Model#equals is used in the following snippet

const alreadyAssociated =
!oldInstance || !associatedInstanceOrPk
? false
: associatedInstanceOrPk instanceof Model
? associatedInstanceOrPk.equals(oldInstance)
: // @ts-expect-error -- TODO: what if the target has no primary key?
oldInstance.get(this.target.primaryKeyAttribute) === associatedInstanceOrPk;

That also seems to be the only place that we use it internally. Model#equalsOneOf isn't used

Sure, tried to fix 'em on the go, but id didn't fly. I will do it properly once I'm back to my computer and push 🫡

Add proper type declarations for the id attribute and provide all
required model attributes when building test instances.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@maksimf maksimf force-pushed the fix/model-equals-cross-model-comparison branch from e948ad1 to eac6ed2 Compare March 7, 2026 21:40
Model variants created by withScope, withoutScope, or withSchema have
different modelDefinition objects. This caused equals() to return false
for instances of the same logical model when one was loaded via a scoped
query (e.g., in hasOne.set() which uses withoutScope()).

Fix by using isSameInitialModel() to compare base models instead of
comparing modelDefinition directly.
@maksimf maksimf force-pushed the fix/model-equals-cross-model-comparison branch from eac6ed2 to b3d8950 Compare March 8, 2026 10:06
Prevent DB2 constraint introspection from mixing referenced columns
across tables that reuse the same key constraint name, and align DB2
query-generator expectations accordingly.
@maksimf maksimf requested a review from WikiRik March 8, 2026 14:58
@WikiRik WikiRik enabled auto-merge (squash) March 8, 2026 17:42
@WikiRik WikiRik merged commit cae4c9c into sequelize:main Mar 8, 2026
53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(core): Model#equals always returns true when comparing instances of different models

3 participants