Skip to content

Fix #3472: allow None for nullable FK/OneToOne _id lookups - #3476

Open
usamashehab wants to merge 3 commits into
typeddjango:masterfrom
usamashehab:fix-3472-fk-id-nullable-lookup
Open

Fix #3472: allow None for nullable FK/OneToOne _id lookups#3476
usamashehab wants to merge 3 commits into
typeddjango:masterfrom
usamashehab:fix-3472-fk-id-nullable-lookup

Conversation

@usamashehab

Copy link
Copy Markdown
Contributor

Related issues

Fixes #3472

Problem

Filtering on the _id column of a nullable ForeignKey or OneToOneField with None was incorrectly rejected by mypy:

class MyModel(models.Model):
    prospect = models.OneToOneField(OtherModel, blank=True, null=True, on_delete=models.CASCADE)

MyModel.objects.filter(prospect_id=None)  # error: Incompatible type for lookup 'prospect_id': (got "None", expected "str | int")

This has two root causes in _resolve_field_from_parts:

  1. Field name detection: The old check compared field_part against model_name + "_id" (the related model's lowercase name + "_id"). This only worked when the FK field name happened to match the related model name (e.g. publisherPublisher). When the field name differs (e.g. prospectOtherModel), the check "prospect_id" == "othermodel_id" failed, and the field stayed as the RelatedField instead of being resolved to the PK column field.

  2. Nullability: Even when the _id variant was correctly detected (e.g. for publisher_id), the resolved PK field always has null=False. So get_field_lookup_exact_type returned str | int without None, even for nullable FKs where prospect_id=None is valid at the database level.

Fix

  • Replaced the model_name + "_id" check with field.attname, which is the actual column name Django assigns to the FK's _id variant. This works regardless of whether the field name matches the model name.
  • Added is_nullable_fk_id tracking: when a FK _id attname is detected, the original FK's null attribute is preserved and passed through to get_field_lookup_exact_type, so None is accepted for nullable _id lookups.
  • Excluded ManyToManyField via field_part != field.name (M2M fields have attname == name, no _id column variant exists).

AI Policy

  • I have read and agree to the AI Policy, removed any "Co-Authored-By" lines attributing coding agents, and manually reviewed the final result

The _id attname of a ForeignKey or OneToOneField (e.g. prospect_id)
was not correctly resolved when the field name differed from the
related model's lowercase name. The previous check compared field_part
against model_name + '_id', which only worked when the FK name matched
the model name (e.g. publisher -> Publisher).

Fixed by checking field.attname instead, which is the actual column
name Django uses for the _id variant. Also preserves the original FK's
nullability: the underlying PK field is always null=False, but the
column is nullable when the FK has null=True, so None is now accepted
for nullable _id lookups.

ManyToManyField is excluded because its attname equals its name (no
_id column variant exists).

@federicobond federicobond 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.

The bare positional bool returned from _resolve_field_from_parts reads a bit awkwardly, and is discarded at most call sites. It also carries a fairly narrow special-case name.

What do you think about introducing a small namedtuple ResolvedLookupField(field, model, is_nullable), where is_nullable is the effective nullability of the looked-up column? That way the resolver owns the nullability decision and hands it down as a general fact.

…dtuple

Per review feedback from @federicobond: replace the bare positional
bool with a ResolvedLookupField NamedTuple that owns the nullability
decision. The resolver now hands down is_nullable as a general fact
instead of a narrow is_nullable_fk_id special-case.
@usamashehab

This comment was marked as outdated.

@federicobond

Copy link
Copy Markdown
Contributor

Look, if you want to continue contributing to the project I suggest you re-read the AI policy you agreed to.

If I have to respond to a comment written entirely by an LLM I would rather work with the model directly instead of through a slow human intermediary. It's not bad to use AI but if you are not willing to put in the time to write a short reply in your own words it's just not worth it to take your contribution seriously.

@usamashehab

Copy link
Copy Markdown
Contributor Author

Look, if you want to continue contributing to the project I suggest you re-read the AI policy you agreed to.

If I have to respond to a comment written entirely by an LLM I would rather work with the model directly instead of through a slow human intermediary. It's not bad to use AI but if you are not willing to put in the time to write a short reply in your own words it's just not worth it to take your contribution seriously.

my skills in writing is not good, that's why i am using AI to write a good response but seems that is not the best behavior, will try to write on my own

@UnknownPlatypus

Copy link
Copy Markdown
Contributor

my skills in writing is not good, that's why i am using AI to write a good response but seems that is not the best behavior, will try to write on my own

yes pls, it's not an issue if your writing is not perfect, and practice is the only way to make it better.
Don't let AI steal that from you.

Comment on lines +436 to +444
# The lookup uses the FK's ``_id`` suffix (e.g. ``prospect_id``).
# Django's ``get_field`` returns the ``RelatedField`` for this
# attname, but semantically it refers to the underlying column,
# so resolve to the related model's primary key field instead.
# Preserve the original FK's nullability — the PK field itself
# is always ``null=False`` but the column is nullable when the
# FK is.
# ``field_part != field.name`` excludes ManyToManyField, whose
# ``attname`` equals its ``name`` (no ``_id`` column variant).

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.

Can you simplify this comment ? lot's of noise
It should focus on explaining the if conditional, mostly the last 2 lines I think ?

Also let the comment take more width and avoid breaking in the middle of sentences, it should be readable for a human as much as possible

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Cannot filter with None on _id attributes of FK/OneToOne

3 participants