Skip to content

fix(persistence): keep many-to-many junction rows when the relation is not loaded - #12711

Merged
alumni merged 1 commit into
typeorm:masterfrom
IbrahimHafez1:fix/many-to-many-undefined-relation-batch-save
Jul 29, 2026
Merged

alumni merged 1 commit into
typeorm:masterfrom
IbrahimHafez1:fix/many-to-many-undefined-relation-batch-save

Conversation

@IbrahimHafez1

@IbrahimHafez1 IbrahimHafez1 commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #12710

Description of change

Saving several entities in one save() call (or via a cascade) silently deleted every many-to-many junction row of any entity in the batch whose relation property was not loaded, whenever another entity in the same persistence group had that relation loaded.

Root cause: the prefer-nullish-coalescing style refactor (cab8fcb, #12340) changed ManyToManySubjectBuilder.buildForSubjectRelation from

if (relatedEntities === null) relatedEntities = []
if (!Array.isArray(relatedEntities)) return

to

relatedEntities ??= []
if (!Array.isArray(relatedEntities)) return

??= also matches undefined, so an unloaded relation — which previously fell through to the !Array.isArray guard and was skipped — became "relation set to empty, remove all junction rows". The removal only materializes when another subject in the same group has the relation loaded (that makes SubjectDatabaseEntityLoader fetch the database-side relation ids for the whole target group), which is why single-entity saves look fine and the data loss surfaces on batch saves and cascades. The same refactor kept the explicit undefined → return guard in the sibling OneToManySubjectBuilder — only the many-to-many builder lost it.

Fix: restore the undefined → skip guard, mirroring OneToManySubjectBuilder. null still means "remove all bindings" and non-array values (e.g. lazy-relation promises) are still skipped, so the documented 1.0 semantics are all preserved:

  • undefined (not loaded) → junction rows left intact (this was broken)
  • null / [] → all junction rows removed (unchanged, still covered by the existing tests in the same file)

Verification:

Pull-Request Checklist

…t loaded

A batch save() (or cascade) deleted every junction row of an entity
whose many-to-many relation property was undefined, whenever another
entity in the same persistence group had that relation loaded.

Restore the undefined-means-skip guard that was accidentally dropped
when the null check was converted to nullish coalescing, mirroring the
guard OneToManySubjectBuilder kept in the same refactor.

Fixes typeorm#12710
@github-actions github-actions Bot added the linked-issue PR references an issue label Jul 18, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.

Qodo Logo

@pkg-pr-new

pkg-pr-new Bot commented Jul 18, 2026

Copy link
Copy Markdown
  • typeorm-sql-js-example

    npm i https://pkg.pr.new/typeorm@12711
    

commit: 4e51480

@alumni
alumni merged commit 04ff4da into typeorm:master Jul 29, 2026
48 checks passed
@github-actions github-actions Bot added this to the 2.0.next milestone Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: persistence linked-issue PR references an issue

Development

Successfully merging this pull request may close these issues.

Batch save() silently deletes many-to-many junction rows of entities saved without their relation loaded (v1.0 regression)

3 participants