-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add failing test for saving with a new sort_order #36
Conversation
task = Task.objects.create() | ||
|
||
for order in sort_orders: | ||
SubTask.objects.create(task=task, sort_order=order) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The minimal failing examples all look like this:
sort_orders = [n, n + 1, 1]
The create
fails in Orderable._save
when it tries to run .update(sort_order=F('sort_order') + 1)
on the first two SubTask
instances.
I think the problem is that the first SubTask
with sort_order == n
is incremented to sort_order == n + 1
which conflicts with the second SubTask
because of the unique_together
between sort_order
and task
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't occur with the Task
model because it doesn't have any unique
constraints.
@meshy I'm not sure how best to fix this. Any ideas? |
qs.update(sort_order=models.F('sort_order') + 1) | ||
except IntegrityError: | ||
for obj in qs.order_by('-sort_order'): | ||
qs.filter(pk=obj.pk).update(sort_order=models.F('sort_order') + 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is ugly, but it works...
Unfortunately, Django doesn't have support for DEFERRABLE
unique constraints. Ideally, we'd be able to set the constraint as UNIQUE DEFERRABLE INITIALLY IMMEDIATE
and then run the update
with DEFERRED
set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem right to me -- why is there an integrity error in the first place? The "shift" should have cleared the way, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Please bear in mind that I don't really understand the failing case here.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Say we have a qs
where sort_order
looks like this: 1, 2, 3, 4
.
We add a new item with sort_order == 2
.
Orderable._save
creates to_shift
which looks like: 2, 3, 4
.
The old code then tried to call to_shift.update(sort_order = F('sort_order') + 1)
.
This works fine without any unique
constraints, but if sort_order
is unique_together
with another field (as in SubTask
) the update
will raise IntegrityError
when it increments 2
to 3
. The internal state of to_shift
is now 3, 3, 4
, which is invalid.
If Django (and the db) supported DEFERRED
, we could defer the unique check until the update
had completed (3, 4, 5
) and everything would work as in the case without unique
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like a manifestation of #7
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyway, I agree this isn't really the "right" fix, but I don't think I can implement the "right" fix without DEFERRED
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about defining the new sort order by: Max(sort_order) + sort_order + 1
? Would that work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice idea. I tried a few variants of it - the first just led to IntegrityError
for a slightly more complex example. The second caused sort_order
to increase exponentially - I get django.db.utils.DataError
eventually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool thanks :)
Current coverage is
|
@kevinetienne @adam-thomas Review? |
@@ -207,3 +213,13 @@ def test_changing_parent(self): | |||
subtask_2.task = task | |||
subtask_2.save() | |||
self.assertSequenceEqual(task.subtask_set.all(), [subtask, subtask_2]) | |||
|
|||
@given(lists(integers(min_value=1), min_size=1, unique=True)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Populates the sort_order
parameter with a list of positive integers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than installing Hypothesis, wouldn't it be better just to do sort_orders = range(1, n)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(With sort_orders.shuffle()
if you need them to be randomly distributed. Or writing out a short list by hand.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is much more thorough. I don't trust us to catch all the edge cases without hypothesis.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll also note that it found an overflow in the sort_order
field with one of my attempts to improve _update
. This is the sort of edge-case that I would not have found with a simple test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found the bugs how? It looks like it improved on the hardcoded list in the test that was there already, but wouldn't changing that to [1,2,3,4]
as you described earlier have achieved the same thing? Did you get that from the test output? Do you think you would have been able to find this error using Hypothesis without that test already in place (or without writing it yourself)?
And yes - the point being that there probably aren't many more edge cases or failures in the exact list of sort_orders
you have (there are only so many different relevant situations you can construct), but there might be plenty of edge cases or failures elsewhere that this test will never hit. Adding Hypothesis here doesn't guarantee much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll also note that it found an overflow in the sort_order field with one of my attempts to improve _update. This is the sort of edge-case that I would not have found with a simple test.
That sounds good, although I'm a bit surprised/alarmed that sort_order
can overflow at all outside of a case where we've basically filled up the database, at which point we have other issues. Theoretically any field with a limited size can overflow in that situation, so I don't know if there's any value in designing around it.
Is there a test for this? What else did it find? If I'm missing information about what you've actually done here, please post it either in the issue or in the code itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@adam-thomas @kevinetienne Merge? |
Merging, other issues if needed can be done in other PRs |
Add failing test for saving with a new sort_order
|
Fix
IntegrityError
inOrderable.save
whenqs.update
violates aunique
constraint.I used hypothesis because
Orderable.save
is very complex and has potentially many breaking edge-cases hidden away.