Skip to content

fix(relations): support models whose primary key is a OneToOneField - #2299

Open
d3vyce wants to merge 1 commit into
tortoise:developfrom
d3vyce:fix/o2o-primary-key
Open

d3vyce wants to merge 1 commit into
tortoise:developfrom
d3vyce:fix/o2o-primary-key

Conversation

@d3vyce

@d3vyce d3vyce commented Oct 6, 2026

Copy link
Copy Markdown

Description

Fixes three bugs with a model whose primary key is a OneToOneField. Both the relation and its <field>_id backing field are flagged pk, and these code paths took the relation, which has no column:

class User(Model):
    name = fields.CharField(50)

class Profile(Model):
    user = fields.OneToOneField("models.User", primary_key=True, related_name="profile")
    bio = fields.TextField()

class Post(Model):
    profile = fields.ForeignKeyField("models.Profile", related_name="posts")

# before: the "post" table has no "profile_id" column, the link is never written or read back
await Profile.get(pk=1)                          # FieldError: Unknown filter param 'pk'
await Profile.filter(user__name="ada").delete()  # KeyError: 'user' (same for update())

A ForeignKeyField to such a model now targets the backing field. Relations are set up in model name order, so when Post comes first, Profile.user is now initialized first, with or without to_field="user_id". That is also what makemigrations now writes: it wrote to_field='user', and the migrated table had no column either. Table names are now assigned before any relation, as Profile.user names its reverse relation after its table.

add_field() registers the pk filters for the backing field, which _init_relations adds after the class is created, and _pk_in_subquery() (#2269) only selects the pk fields that have a column.

Motivation and Context

I found no issue for these; related to #1771 / #1783 (many-to-many on such a model). A one-to-one primary key extends a table row for row, and other tables reference the extension: their foreign key silently stored nothing.

How Has This Been Tested?

New tests in tests/test_relations.py (foreign keys from models set up before and after the target, pk filters) and tests/test_queryset.py (delete and update through a related field), run on SQLite and PostgreSQL 18 (asyncpg). The full suite passes on SQLite and asyncpg. MySQL and MSSQL are left to CI.

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added the changelog accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

This fix has been partially implemented by Claude Opus 5.5. All modifications made by the AI have been reviewed by me.

@d3vyce
d3vyce force-pushed the fix/o2o-primary-key branch from 85d2908 to 39ac9e9 Compare October 6, 2026 21:01
@codspeed

codspeed Bot commented Oct 7, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing d3vyce:fix/o2o-primary-key (39ac9e9) with develop (1f896c0)

Open in CodSpeed

@d3vyce
d3vyce force-pushed the fix/o2o-primary-key branch from 39ac9e9 to d577f5e Compare October 7, 2026 16:01

This branch has not been deployed

No deployments
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