Opened 14 years ago
Closed 10 years ago
#17186 closed New feature (duplicate)
Inverted F expression (negation)
| Reported by: | Andrei Antoukh | Owned by: | nobody | 
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | dev | 
| Severity: | Normal | Keywords: | |
| Cc: | Triage Stage: | Accepted | |
| Has patch: | yes | Needs documentation: | yes | 
| Needs tests: | yes | Patch needs improvement: | yes | 
| Easy pickings: | no | UI/UX: | no | 
Description
By default, the expression F django, can not make something like:
UPDATE "testmodel" SET "done" = NOT "testmodel"."done";
The idea is to make updates or queries like this:
TestModel.objects.update(done=~F('done'))
Taking as a reference, this model:
class TestModel(models.Model): done = models.BooleanField(default=False)
Attached is a patch that works for me, but not if it's the best way to do it.
Attachments (4)
Change History (17)
by , 14 years ago
| Attachment: | invert.f.patch added | 
|---|
comment:1 by , 14 years ago
| Has patch: | set | 
|---|
comment:2 by , 14 years ago
| Component: | Uncategorized → Database layer (models, ORM) | 
|---|---|
| Needs tests: | set | 
| Triage Stage: | Unreviewed → Accepted | 
Seems reasonable, and I can't think of an alternative use of negating an F object.
by , 14 years ago
| Attachment: | invert.f-2.patch added | 
|---|
comment:3 by , 14 years ago
| Needs tests: | unset | 
|---|
I modified a little test, to cover this modification. Attached the new patch.
comment:4 by , 14 years ago
There are several problems with this patch.
For example, you should be able to negate compound expressions:
>>> Demo.objects.filter(a_field=~(F('a_field')+1)) Traceback (most recent call last): File "<console>", line 1, in <module> TypeError: bad operand type for unary ~: 'ExpressionNode'
Secondly, there is no indication of negation in the string representation, like there is for other operators:
>>> print ~F('field') (DEFAULT: ) >>> print F('field') + F('other') (+: (DEFAULT: ), (DEFAULT: ))
Thirdly, tree.Node already supplies a negate() function and a negated field.  Why not use those instead?
I'm not entirely pleased with my alternative as it still doesn't make use of connector, but it solves these listed problems.
by , 14 years ago
| Attachment: | 17186-3.patch added | 
|---|
comment:5 by , 14 years ago
Your solution seems to me pretty good. I had not thought of fields that are not Boolean. In fact it was an idea that surely someone could have implemented better than me. (just this case).
comment:6 by , 14 years ago
| Needs documentation: | set | 
|---|---|
| Needs tests: | set | 
| Patch needs improvement: | set | 
As a new feature this needs documentation. Also:
- What's the use case for negation on non-boolean values? How does this behave on different DBs (the tests should check that).
 - No tests for compounds statements (should they be boolean only?).
 - Why 
~F()instead of more Pythonicnot F(). After all~Trueis-2notFalse. You could usenotfor boolean expressions and~for bit negation on fields (which might be useful too!). 
comment:7 by , 14 years ago
One more thing worth documenting, due to difference in None and NULL semantics:
class A(Model): f = NullBooleanField() for m in A.objects.all(): m.f = not m.f; m.save() # all NULL values are now True # while this will leave NULL values unchanged as NOT NULL is NULL A.objects.update(f=not F('f'))
follow-up: 9 comment:8 by , 14 years ago
Regarding use case for negation on non-boolean values: That's a good question; I'm not sure that there is one.  I provided that only as a stupid example of lack of composability (at the db level negation on non-boolean values can work, but that doesn't mean it is useful).  If you can propose a test, I would be glad to include it.  Altogether, though, that does raise a good question - so far, all F() objects are generally composable across all operations, and I would not want to break that by adding another operation.  Not all computations in general are composable, though; should F() object composability reflect that?  Now my head hurts.
Regarding "Why ~F() instead of not F()": there are two reasons.  One: To match with Q objects; Two, and more importantly: python itself does not provide a __not__() method for object instances.  So yes, you are absolutely right, but I don't think it can be done.
As to your other points, well taken.  I hope my updated patch is an improvement.
comment:9 by , 14 years ago
Replying to nate_b:
Regarding "Why ~F() instead of not F()": there are two reasons. One: To match with Q objects; Two, and more importantly: python itself does not provide a __not__() method for object instances. So yes, you are absolutely right, but I don't think it can be done.
Ok, now I feel stupid ;) 
As to your other points, well taken. I hope my updated patch is an improvement.
The patch looks good. Now we just need to work out the non-boolean cases somehow.
comment:10 by , 14 years ago
Apparently various database engines work differently regarding negation of non-boolean values. For example on Postgres each of the following queries raises an error:
SELECT NOT 5; SELECT NOT "5"; SELECT NOT "5abc";
while on SQLite it happily returns 0. SQLite casts the expression to a NUMERIC value when used in a boolean context, so in each case it is "truthy". 
 
comment:11 by , 14 years ago
| Triage Stage: | Accepted → Design decision needed | 
|---|
I guess it's a DDN. Generally it is even possible to do something like that in SQLite:
Company.objects.update(name=F("name") * F("name"))
And the name field after that is "0". This is rather a wider case, whether the ORM should allow silly expressions like that, and not only negation of non-booleans. It is completely DBMS-dependent and most "true" DB engines will barf immediately.  
comment:12 by , 13 years ago
| Triage Stage: | Design decision needed → Accepted | 
|---|
This feature shouldn't be expected to give meaningful results on anything other than a boolean value.
I forgot this:
The result of the sql would look like:
(tested on sqlite3, postgresql and mysql)