I came across this on my inherited models. When I exclude using the base model qs, I get the expected results but when I do it from the derived model the null clause is left out. See below.
class TestBase(models.Model):
test_field = models.DateField(null = True)
class TestDerived(TestBase):
pass
>>> TestDerived.objects.create()
<TestDerived: TestDerived object>
>>> TestDerived.objects.create(test_field=datetime.date.today())
<TestDerived: TestDerived object>
>>> TestDerived.objects.exclude(test_field__lt = datetime.date.today())
[<TestDerived: TestDerived object>]
>>> TestBase.objects.exclude(test_field__lt = datetime.date.today())
[<TestBase: TestBase object>, <TestBase: TestBase object>]
>>> TestBase.objects.exclude(test_field__lt = datetime.date.today()).query.as_sql()
('SELECT `signets_testbase`.`id`, `signets_testbase`.`test_field` FROM `signets_testbase` WHERE NOT ((`signets_testbase`.`test_field` < %s AND NOT (`signets_testbase`.`test_field` IS NULL)))', ('2010-10-08',))
>>> TestDerived.objects.exclude(test_field__lt = datetime.date.today()).query.as_sql()
('SELECT `signets_testbase`.`id`, `signets_testbase`.`test_field`, `signets_testderived`.`testbase_ptr_id` FROM `signets_testderived` INNER JOIN `signets_testbase` ON (`signets_testderived`.`testbase_ptr_id` = `signets_testbase`.`id`) WHERE NOT (`signets_testbase`.`test_field` < %s )', ('2010-10-08',))
I've nailed it to this particular piece of code from query.py:
Basically what happens is that when you query from a derived class you fall into the first if len(join_list) > 1 but since the following part is a elif you never get there. Hence when a nullable derived field is excluded (negated in the code) the null case is simply ignored. I believe transforming the elif into a if fixes the problem (it does for me at least!). This code is from trunk but this bug has been there for some time (I'm using 1.1.2).