Skip to content

Fix ContentReplicaRouter to allow relations across default/replica - #1503

Merged
YasenT merged 1 commit into
pulp:mainfrom
YasenT:fix-content-replica-router-allow-relation
Sep 21, 2026
Merged

YasenT merged 1 commit into
pulp:mainfrom
YasenT:fix-content-replica-router-allow-relation

Conversation

@YasenT

@YasenT YasenT commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

ContentReplicaRouter didn't implement allow_relation, so Django fell back to its default same-database check. Domains read during content app requests come from "replica", but pulp_maven's path_index builds a new, unsaved Artifact (db=None) and assigns that domain to it. Since "replica" != None, Django raised:

ValueError: Cannot assign "<Domain: ...>": the current database
router prevents this relation.

"replica" mirrors "default", so relations between objects loaded from either database are always safe.

Summary by Sourcery

Permit safe relations across mirrored default and replica databases in the content database router.

Bug Fixes:

  • Allow relations between objects from the replica, default, and unassigned databases to prevent content request failures when assigning replica-read objects to new instances.

Tests:

  • Add coverage confirming the database router permits cross-database and unassigned-object relations.

ContentReplicaRouter didn't implement allow_relation, so Django fell
back to its default same-database check. Domains read during content
app requests come from "replica", but pulp_maven's path_index builds a
new, unsaved Artifact (db=None) and assigns that domain to it. Since
"replica" != None, Django raised:

  ValueError: Cannot assign "<Domain: ...>": the current database
  router prevents this relation.

"replica" mirrors "default", so relations between objects loaded from
either database are always safe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

The router now explicitly permits all model relations, avoiding Django’s same-database rejection when replica-loaded domains are assigned to new unsaved objects, with parameterized tests covering replica, default, and unset database states.

Sequence diagram for replica-read relation assignment

sequenceDiagram
    participant ContentApp
    participant Router as ContentReplicaRouter
    participant Replica
    participant Artifact
    participant Domain

    ContentApp->>Replica: db_for_read(Domain)
    Replica-->>ContentApp: Domain(db=replica)
    ContentApp->>Artifact: Create unsaved Artifact(db=None)
    ContentApp->>Router: allow_relation(Artifact, Domain)
    Router-->>ContentApp: True
    ContentApp->>Artifact: Assign Domain relation
Loading

File-Level Changes

Change Details Files
Allow relations between objects regardless of database assignment because the replica mirrors the default database.
  • Add an allow_relation method that unconditionally returns True.
  • Document the safety rationale for replica/default and unsaved-object relations.
pulp_service/pulp_service/app/database_router.py
Add coverage for cross-database and unsaved-object relation handling.
  • Parameterize router tests across replica, default, and unset database states.
  • Verify allow_relation returns True for each supported combination.
pulp_service/pulp_service/tests/unit/test_database_router.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="pulp_service/pulp_service/app/database_router.py" line_range="38" />
<code_context>
     def db_for_write(self, model, **hints):
         return "default"

+    def allow_relation(self, obj1, obj2, **hints):
+        # "replica" mirrors "default", so relations between objects loaded from
+        # either database (or not yet assigned to one) are always safe. Without
+        # this, Django's default same-db check rejects assigning a replica-read
+        # object (e.g. a Domain) to a new, unsaved instance (db=None), such as
+        # when pulp_maven builds an Artifact during a content-app read.
+        return True
+
     def allow_migrate(self, db, app_label, model_name=None, **hints):
</code_context>
<issue_to_address>
**issue (broader_impact):** `allow_relation` returns `True` for every pair of objects, including objects loaded from an unrelated database alias such as `other`. When such an object is assigned to an object saved on `default`, Django accepts the relation and the resulting foreign key references the other database's row ID, raising an integrity error when absent or linking to the wrong row when IDs collide.

**Triggers:** When callers explicitly load or construct an object whose `_state.db` is an alias other than `default`, `replica`, or `None`.

**Suggested fix:** Return `True` only when both database states are in the supported `default`/`replica`/`None` set, and return `None` otherwise so Django's normal same-database check applies.

```suggestion
        if {obj1._state.db, obj2._state.db} <= {"default", "replica", None}:
            return True
        return None
```
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and if the router permits an object from an unintended database, Django could persist an incorrect foreign-key or many-to-many association because cross-database constraints are not enforced. Reverting stops the new behavior, but any bad associations already written would need to be identified and repaired or recomputed.

Blocking findings: pulp_service/pulp_service/app/database_router.py:38


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

# this, Django's default same-db check rejects assigning a replica-read
# object (e.g. a Domain) to a new, unsaved instance (db=None), such as
# when pulp_maven builds an Artifact during a content-app read.
return True

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.

issue (broader_impact): allow_relation returns True for every pair of objects, including objects loaded from an unrelated database alias such as other. When such an object is assigned to an object saved on default, Django accepts the relation and the resulting foreign key references the other database's row ID, raising an integrity error when absent or linking to the wrong row when IDs collide.

Triggers: When callers explicitly load or construct an object whose _state.db is an alias other than default, replica, or None.

Suggested fix: Return True only when both database states are in the supported default/replica/None set, and return None otherwise so Django's normal same-database check applies.

Suggested change
return True
if {obj1._state.db, obj2._state.db} <= {"default", "replica", None}:
return True
return None

@YasenT
YasenT merged commit 1731cc9 into pulp:main Sep 21, 2026
6 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.

1 participant