Skip to content

fix(relationships): ignore relationships whose related collection was deleted - #1034

Merged
abnegate merged 2 commits into
mainfrom
fix/orphaned-relationship
Oct 16, 2026
Merged

abnegate merged 2 commits into
mainfrom
fix/orphaned-relationship

Conversation

@abnegate

Copy link
Copy Markdown
Member

Production incident

Cloud 1.51.0 (database 8.3.0): PATCH /v1/tablesdb/{db}/tables/{t}/rows/{rowId} returns 500 Utopia\Database\Exception\NotFound: Collection not found (Trait/Collections.php:203, getCollection()) for a project whose collections hold one-way many-to-one relationship attributes (child side, onDelete setNull / restrict) whose related collection was deleted on 7.x. The related collection's metadata is gone; the relationship attribute is not.

7.4.2's getCollection() returned an empty document for a missing collection, so an untouched relationship was skipped. 8.x's getCollection() throws, and the relationship hook calls it for every relationship on every document operation, so any get, find, create, update, upsert or delete on such a collection fails.

Fix

The relationship hook resolves a relationship's related collection through one helper, relatedCollection(), built on the non-throwing findCollection(). When the related collection is missing, the relationship is treated as a plain attribute:

  • Reads (get, find, populate, relationship selects): no population. The stored value comes back as it is.
  • Writes (create, update, upsert, bulk update): no relating. The side that holds the key stores the ID it is given (null included; a nested document is stored by its ID). A side that stores nothing (one-to-many parent, many-to-one child, many-to-many) drops the value. Untouched values are left alone.
  • Deletes: onDelete restrict, setNull and cascade do nothing.
  • Authorization and nested preparation: skipped. There is nothing to authorize against.

Documents::validateRelationshipSelects() also skips the select validation for a related collection that is missing.

Population now resolves the related collection once per relationship and passes it to the batch loaders. Before, each loader read it, and then it was read again before queueing the next level. Relationship filter queries that reach into a deleted collection (author.name = x) still throw, because there is no schema to query against.

Compared with 7.4.2: an untouched or null orphaned relationship worked there, and still works here. A non-null write threw Collection not found there (relateDocumentsById() → getDocument('', …)). Here it stores the raw ID.

Regression tests

tests/e2e/Adapter/Scopes/Relationships/OrphanedTests.php, part of RelationshipTests, runs on every adapter that supports relationships. The fixture builds articles with:

  • one-way manyToOne author (setNull) and editor (restrict), held by articles
  • the child sides of one-way manyToOne relationships comments (setNull) and likes (restrict), held by other collections (the production shape)
  • oneToMany tags (cascade) and manyToMany topics (restrict)

It then drops each related collection's table and deletes its _metadata row, the state 7.x left.

Test (SQLite, MariaDB, MySQL, Postgres, Memory, Redis, Pool, and the SharedTables variants) origin/main this PR
testOrphanedRelationshipUpdateDocument (the production failure; partial, whole-document, raw-ID, null and stores-nothing writes) ✘ NotFound: Collection not found ✔
testOrphanedRelationshipCreateDocument ✘ NotFound ✔
testOrphanedRelationshipGetDocument (with and without author.* select) ✘ NotFound ✔
testOrphanedRelationshipFind (no select, select(name), select(name, author.*), select(*, author.name, comments.name, tags.name, topics.name), equal(author)) ✘ NotFound ✔
testOrphanedRelationshipDeleteDocument (deleteDocument + deleteDocuments) ✘ NotFound ✔
testOrphanedRelationshipUpsertDocument (update and insert) ✘ NotFound (Memory: n/a, no upserts) ✔
testOrphanedRelationshipUpdateDocuments ✘ NotFound ✔

Before the fix: 83 of 84 adapter×test runs failed with the production exception. The 84th was the Memory upsert test, which skips. After the fix: 84 of 84 pass.

Local verification

  • composer lint (Pint): passed
  • composer check (PHPStan, both configs): no errors
  • The relationship suites (RelationshipTests, Relationships/*, JoinRelationshipTests, 219 tests each) are green on SQLite, MariaDB, MySQL, Postgres, Memory and SharedTables/MySQL.

🤖 Generated with Claude Code

abnegate and others added 2 commits October 16, 2026 18:38
… deleted

7.x deleteCollection() could leave relationship attributes behind on other
collections, pointing at a collection whose metadata no longer exists. 7.x
getCollection() returned an empty document for it, so document operations
effectively skipped such a relationship. 8.x getCollection() throws
NotFound, so every get, find, create, update, upsert and delete on a
collection holding one fails with "Collection not found".

The relationship hook now resolves a relationship's related collection
with findCollection() and treats a missing one as a plain attribute: it is
not related, populated, authorized, cascaded or restricted, and its stored
value is returned as is. A write keeps the ID on the side holding the key
and drops the value from a side that stores nothing.

Population resolves the related collection once per relationship and
passes it to the batch loaders, instead of reading it in each loader and
again before queueing the next level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 27 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: utopia-php/database/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 79f83f9a-b6fb-42aa-9a23-6e01a53ea62d
📥 Commits

Reviewing files that changed from the base of the PR and between 9e664e6 and 771548b.

📒 Files selected for processing: 4
  • src/Database/Hook/Relationships.php
  • src/Database/Trait/Documents.php
  • tests/e2e/Adapter/Scopes/RelationshipTests.php
  • tests/e2e/Adapter/Scopes/Relationships/OrphanedTests.php
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@abnegate
abnegate merged commit 7154053 into main Oct 16, 2026
24 checks passed
@abnegate
abnegate deleted the fix/orphaned-relationship branch October 16, 2026 05:50
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.

1 participant