Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@Eddy-123
Copy link

@Eddy-123 Eddy-123 commented Oct 31, 2025

Trac ticket number

ticket-20024

Branch description

Problem
The SQL generated by an exclude queryset is not semantically aligned with the queryset when using __in lookups containing None.

>>> str(Entry.objects.exclude(foo__in=[None, 1]).query)
'SELECT "core_entry"."id", "core_entry"."foo" FROM "core_entry" WHERE NOT ("core_entry"."foo" IN (1) AND "core_entry"."foo" IS NOT NULL)'

Solution
The PR updates WhereNode to use OR and generate IS NULL for correct SQL semantics in this case.

>>> str(Entry.objects.exclude(foo__in=[None, 1]).query)
'SELECT "core_entry"."id", "core_entry"."foo" FROM "core_entry" WHERE NOT (("core_entry"."foo" IN (1) OR "core_entry"."foo" IS NULL))'

Checklist

  • This PR targets the main branch.
  • The commit message is written in past tense, mentions the ticket number, and ends with a period.
  • I have checked the "Has patch" ticket flag in the Trac system.
  • I have added or updated relevant tests.
  • I have added or updated relevant docs, including release notes if applicable.
  • I have attached screenshots in both light and dark modes for any UI changes.

Copy link
Member

@jacobtylerwalls jacobtylerwalls left a comment

Choose a reason for hiding this comment

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

Thanks for the PR!

@charettes reviewed #19691, so he might have some thoughts.

Copy link
Member

@charettes charettes left a comment

Choose a reason for hiding this comment

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

Looks like the right approach to me!

…lude().

Thanks Simon Charette for the review and Jason Hall for a prior iteration.
Copy link
Member

@jacobtylerwalls jacobtylerwalls left a comment

Choose a reason for hiding this comment

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

Thanks! I reorganized commits slightly (one adding coverage with Refs # and the one fixing the bug and testing it with Fixed #.)

Welcome aboard! ⛵

@jacobtylerwalls
Copy link
Member

buildbot, test on oracle.

def setUpTestData(cls):
cls.tenant = Tenant.objects.create()
cls.user = User.objects.create(
tenant=cls.tenant, id=1, email="[email protected]"
Copy link
Member

Choose a reason for hiding this comment

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

Avoid hardcoding primary key values

Copy link
Author

Choose a reason for hiding this comment

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

Sure. But, considering that the User model in this module uses a composite primary key and auto assignment is not allowed, I have to provide an id when creating the user. The previous tests in this file are handling primary key values in the same way.

Still, your observation applies to the tuples, and I'm considering updating them.

Comment on lines +1635 to +1641
if (
lookup_type == "in"
and isinstance(condition.rhs, Iterable)
and not isinstance(condition.rhs, (str, bytes))
and any(v is None for v in condition.rhs)
):
clause.add(lookup_class(col, True), OR)
Copy link
Member

Choose a reason for hiding this comment

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

This monstrosity seems to merit an explanatory comment.

Copy link
Author

@Eddy-123 Eddy-123 Nov 14, 2025

Choose a reason for hiding this comment

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

Sure, it does look a bit monstrous. In short, it targets in lookups where the rhs iterable (non-string) contains None. If you have any suggestion to simplify the condition, please let me know.

In the first comment, the issue I'm highlighting from the sql generated is that AND is generated instead of OR, on the other hand IS NOT NULL is generated instead of IS NULL.

clause.add(lookup_class(col, True), OR) corrects this behavior using True to generate IS NULL with OR connector.

Copy link
Member

Choose a reason for hiding this comment

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

I think Tim is encouraging adding a comment above this if block in the code.

Comment on lines +620 to +625
def test_pk_in_with_partial_or_all_none(self):
qs = User.objects
self.assertQuerySetEqual(qs.filter(pk__in=[(1, None)]), [])
self.assertQuerySetEqual(qs.filter(pk__in=[(None, None)]), [])
self.assertQuerySetEqual(qs.exclude(pk__in=[(1, None)]), [self.user])
self.assertQuerySetEqual(qs.exclude(pk__in=[(None, None)]), [self.user])
Copy link
Member

Choose a reason for hiding this comment

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

If you're going to use a test class for this, perhaps each assertion could be its own method.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants