Skip to content
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

Merged
merged 9 commits into from
Jan 12, 2016
Merged

Conversation

LilyFoote
Copy link
Collaborator

Fix IntegrityError in Orderable.save when qs.update violates a unique constraint.

I used hypothesis because Orderable.save is very complex and has potentially many breaking edge-cases hidden away.

@LilyFoote LilyFoote self-assigned this Jan 11, 2016
@LilyFoote LilyFoote added the bug label Jan 11, 2016
task = Task.objects.create()

for order in sort_orders:
SubTask.objects.create(task=task, sort_order=order)
Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

@LilyFoote
Copy link
Collaborator Author

@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)
Copy link
Collaborator Author

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.

Copy link
Collaborator

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?

Copy link
Collaborator

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.)

Copy link
Collaborator Author

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.

Copy link
Collaborator

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It definitely looks related to #26 / #33.

Copy link
Collaborator Author

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.

Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

cool thanks :)

@codecov-io
Copy link

Current coverage is 81.42%

Merging #36 into master will increase coverage by +0.97% as of 497679a

@@            master     #36   diff @@
======================================
  Files            2       2       
  Stmts          133     140     +7
  Branches         0       0       
  Methods          0       0       
======================================
+ Hit            107     114     +7
  Partial          0       0       
  Missed          26      26       

Review entire Coverage Diff as of 497679a


Uncovered Suggestions

  1. +7.86% via orderable/admin.py#52...62
  2. +4.29% via orderable/admin.py#23...28
  3. +2.15% via orderable/models.py#63...65
  4. See 3 more...

Powered by Codecov. Updated on successful CI builds.

@LilyFoote
Copy link
Collaborator Author

@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))
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this do?

Copy link
Collaborator Author

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.

Copy link
Contributor

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)?

Copy link
Contributor

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.)

Copy link
Collaborator Author

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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

http://persephonemagazine.com/wp-content/uploads/2013/01/gw-itcrowdmosspopcorn.gif

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@LilyFoote
Copy link
Collaborator Author

@adam-thomas @kevinetienne Merge?

@kevinetienne
Copy link
Contributor

Merging, other issues if needed can be done in other PRs

kevinetienne pushed a commit that referenced this pull request Jan 12, 2016
Add failing test for saving with a new sort_order
@kevinetienne kevinetienne merged commit 2495aed into master Jan 12, 2016
@kevinetienne kevinetienne deleted the test-reorder branch January 12, 2016 15:42
@kevinetienne
Copy link
Contributor

changelog.md :p

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants