The xor operator emulation for postgres is in the process of being corrected to behave like a true binary operator, meaning that 1 ^ 1 ^ 1 will be true for Postgres.
In order to keep what some regard as “true” xor behaviour between nary operands, do we want to keep the current Postgres emulation as a separate database function? I agree that we shouldn’t provide every possible conceived utility function so wanted to ask here first.
If so what should it be called “xor()” or another suggestion “exactly_one()”?
I think this is the kind of tiny, unlikely-to-be-popular utility that’s better to share via a code snippet, perhaps in a forum or blog post. We’ve resisted adding complete coverage of database functions and I think this would fit in the same bucket.
Thanks I agree but it’s good to get closure on that thread
It’s an unfortunate change for my team as we use the old behaviour a lot but have never had the need for the new “return true if an odd number of operands are true” behaviour… oh well
Sorry @shangxiao for not answering sooner! I’m just catching up with email after the feature freeze and alpha release. I believe is too late now , but if consensus is reached, I’m happy to help you push this forward!
Personally, I did expect the xor to be exactly one until we had the enlightening conversation in the PR, and I think it would be handy to have such functionality in the ORM. Though regarding naming, that’s a tough one! I asked chatGPT about this and it gave me many confusing answers, so I’m not sure there is a “standard name” for this operation.
I think I’ll just add a gist or something to share with people (or maybe even a django-extensions extension like I suggested to the other person ). A doc note or a section in “topics” about combining xor (^) expressions isn’t what people may expect could also be nice, along with an example function to copy-paste.
Re naming; My opinion is that XOR is fine – as a function that takes n arguments – nary XOR should be exclusive as the name suggests
Hey, I just wanted to share my implementation of the n-ary XOR function.
I do think that it would be nice to put this back in the framework (maybe not this implementation), as I don’t think the implementation is trivial. The use case of ‘only one field is set’ using a CheckContraint is a real use case that I find on many of my projects.
Moreover, most of my colleagues weren’t aware of the behavior of XOR with more than 2 operands.
def only_one(**kwargs):
"""
This functions returns an Expression which is true if and only if one the
provided conditions is True.
For example : `only_one(foo=True, bar__isnull=False, foobar="Titi")`
returns :
```
(Q(foo=True) & ~Q(bar__isnull=False) & ~Q(foobar="Titi")) |
(~Q(foo=True) & Q(bar__isnull=False) & ~Q(foobar="Titi")) |
(~Q(foo=True) & ~Q(bar__isnull=False) & Q(foobar="Titi"))
```
"""
def rotate(source_list, amount):
"""
This function performs list rotation. For example:
rotate([1, 2, 3], 1) => [3, 1, 2].
The implementation is from https://stackoverflow.com/a/9457923/6327520
"""
return source_list[-amount:] + source_list[:-amount]
result = None
subexpression_lists = [
rotate(list(kwargs.items()), rotate_amount) for rotate_amount in range(len(kwargs))
]
for valid_expression, *other_expression in subexpression_lists:
final_expression = Q(valid_expression)
for expression in other_expression:
final_expression &= ~Q(expression)
if result is None:
result = final_expression
else:
result |= final_expression
return result
@shangxiao Did you end up sharing your implementation ?
On the surface, that looks like a lot of excess conditional values being evaluated, but I know the query planner has a lot of flexibility in how that actually ends up being calculated. It would be interesting to see the output of an explain analyze from the queries being generated.
I took a slightly different approach. I use the Case / When operators to ensure each condition was only evaluated once.
The simplified version is:
def only_one(**kwargs):
clauses= [
Case(When(**{k:Value(v)}, then=Value(1)),default=Value(0))
for k, v in kwargs.items()
]
return reduce(__add__, clauses)
Which could be used in an annotation clause:
test = only_one(foo=True, bar__isnull=False, foobar="Titi")
queryset = queryset.annotate(number_true=test).filter(number_true=1)
(I describe this as the “simplified” method, because the full implementation is implemented as a model manager method, with additional conditions and tests to allow for field references (F objects) on the right-hand side of the conditions.)