Merge lp:~lifeless/launchpad/bug-752153 into lp:launchpad

Proposed by Robert Collins
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 13392
Proposed branch: lp:~lifeless/launchpad/bug-752153
Merge into: lp:launchpad
Diff against target: 247 lines (+22/-102)
8 files modified
lib/canonical/launchpad/doc/batch_navigation.txt (+4/-74)
lib/lp/app/browser/root.py (+12/-20)
lib/lp/app/browser/tests/launchpad-search-pages.txt (+0/-2)
lib/lp/bugs/stories/webservice/xx-bug-tracker.txt (+1/-1)
lib/lp/bugs/stories/webservice/xx-bug.txt (+2/-2)
lib/lp/services/features/browser/tests/test_changelog.py (+1/-1)
lib/lp/translations/browser/tests/pofile-views.txt (+1/-1)
versions.cfg (+1/-1)
To merge this branch: bzr merge lp:~lifeless/launchpad/bug-752153
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code Approve
Review via email: mp+56505@code.launchpad.net

Commit message

[r=stevenk][bug=752153] Update lazr.batchnavigator to 1.2.4.

Description of the change

Update our batchnavigator dependency to 1.2.4 which supports database friendly batching (including stable batchs that don't skip/repeat things when folk remove or add items).

There is one caveat which is that the ListRangeFactory will, for 'last' links, do one more count(*) than we do at the moment; I think that this will break things that are already basically broken, or be harmless, and so am going to leave it in place for now. The custom IRangeFactories we will write for complex collections will be able to avoid this - and its needed because of bug 239767.

QA for this should involve checking some api collections as well as e.g. search result pagination. Only the OOPS report will weigh in on the last link change impact.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/doc/batch_navigation.txt'
2--- lib/canonical/launchpad/doc/batch_navigation.txt 2011-04-12 11:36:11 +0000
3+++ lib/canonical/launchpad/doc/batch_navigation.txt 2011-04-12 20:07:39 +0000
4@@ -47,88 +47,18 @@
5 ... 'Cupid', 'Donner', 'Blitzen', 'Rudolph']
6
7
8-Performance with SQLObject
9-==========================
10-
11-This section demonstrates that batching generates sensible SQL queries when
12-used with SQLObject, i.e. that it puts appropriate LIMIT clauses on queries.
13-
14-Imports and initialization:
15-
16- >>> from lp.testing.pgsql import CursorWrapper
17- >>> from canonical.launchpad.database.emailaddress import EmailAddress
18- >>> from canonical.launchpad.interfaces.lpstorm import IStore
19- >>> ignore = IStore(EmailAddress) # Prime the database connection.
20- ... # (Priming is important if this test is run in isolation).
21- >>> CursorWrapper.record_sql = True
22-
23-Prepare a query, and create a batch of the results:
24-
25- >>> select_results = EmailAddress.select(orderBy='id')
26- >>> batch_nav = BatchNavigator(select_results, build_request(), size=10)
27- >>> email_batch = batch_nav.currentBatch()
28- >>> batch_items = list(email_batch)
29-
30-Because we're only looking at the first batch, the database is only
31-asked for the first 11 rows. (lazr.batchnavigator asks for 11 instead
32-of 10 so that it can reliably detect the end of the dataset).
33-
34- >>> len(CursorWrapper.last_executed_sql)
35- 1
36- >>> print CursorWrapper.last_executed_sql[0]
37- SELECT ... FROM EmailAddress ... LIMIT 11...
38-
39-Get the next 10. The database is only asked for the next 11 rows:
40-
41- >>> CursorWrapper.last_executed_sql = []
42- >>> email_batch2 = email_batch.nextBatch()
43- >>> batch_items = list(email_batch2)
44- >>> len(CursorWrapper.last_executed_sql)
45- 1
46- >>> CursorWrapper.last_executed_sql[0].endswith('LIMIT 11 OFFSET 10')
47- True
48-
49-As seen above, simply accessing the batch doesn't trigger a SQL query
50-asking for the length of the entire resultset. But explicitly asking
51-for the length will trigger a SQL query in most circumstances.
52-
53- >>> CursorWrapper.last_executed_sql = []
54- >>> ignored = email_batch.total()
55- >>> print CursorWrapper.last_executed_sql[0]
56- SELECT COUNT(*) FROM EmailAddress
57-
58-There are exceptions. When the current batch is the last one in the
59-list, it's possible to get the length of the entire resultset without
60-triggering a COUNT query.
61-
62- >>> CursorWrapper.last_executed_sql = []
63- >>> batch_nav = BatchNavigator(select_results, build_request(), size=50)
64- >>> final_batch = batch_nav.currentBatch().nextBatch()
65- >>> batch_items = list(final_batch)
66- >>> ignored = final_batch.total()
67- >>> print "\n".join(CursorWrapper.last_executed_sql)
68- SELECT ... FROM EmailAddress ... OFFSET 0
69- SELECT ... FROM EmailAddress ... OFFSET 50
70-
71-When the current batch contains the entire resultset, it's possible to
72-get the length of the resultset without triggering a COUNT query.
73-
74- >>> CursorWrapper.last_executed_sql = []
75- >>> one_page_nav = BatchNavigator(select_results, build_request(), size=200)
76- >>> only_batch = one_page_nav.currentBatch()
77- >>> batch_items = list(only_batch)
78- >>> ignored = only_batch.total()
79- >>> print "\n".join(CursorWrapper.last_executed_sql)
80- SELECT ... FROM EmailAddress ... OFFSET 0
81-
82 Multiple pages
83 ==============
84
85 The batch navigator tells us whether multiple pages will be used.
86
87+ >>> from canonical.launchpad.database.emailaddress import EmailAddress
88+ >>> select_results = EmailAddress.select(orderBy='id')
89+ >>> batch_nav = BatchNavigator(select_results, build_request(), size=50)
90 >>> batch_nav.has_multiple_pages
91 True
92
93+ >>> one_page_nav = BatchNavigator(select_results, build_request(), size=200)
94 >>> one_page_nav.has_multiple_pages
95 False
96
97
98=== modified file 'lib/lp/app/browser/root.py'
99--- lib/lp/app/browser/root.py 2011-04-12 11:36:11 +0000
100+++ lib/lp/app/browser/root.py 2011-04-12 20:07:39 +0000
101@@ -13,6 +13,7 @@
102 import time
103
104 import feedparser
105+from lazr.batchnavigator import ListRangeFactory
106 from lazr.batchnavigator.z3batching import batch
107 from zope.component import getUtility
108 from zope.schema.interfaces import TooLong
109@@ -560,6 +561,7 @@
110 class GoogleBatchNavigator(BatchNavigator):
111 """A batch navigator with a fixed size of 20 items per batch."""
112
113+ _batch_factory = WindowedListBatch
114 # Searches generally don't show the 'Last' link when there is a
115 # good chance of getting over 100,000 results.
116 show_last_link = False
117@@ -567,7 +569,9 @@
118 singular_heading = 'page'
119 plural_heading = 'pages'
120
121- def __init__(self, results, request, start=0, size=20, callback=None):
122+ def __init__(self, results, request, start=0, size=20, callback=None,
123+ transient_parameters=None, force_start=False,
124+ range_factory=None):
125 """See `BatchNavigator`.
126
127 :param results: A `PageMatches` object that contains the matching
128@@ -578,25 +582,13 @@
129 :param size: The batch size is fixed to 20, The param is not used.
130 :param callback: Not used.
131 """
132- # We do not want to call super() because it will use the batch
133- # size from the URL.
134- # pylint: disable-msg=W0231
135 results = WindowedList(results, start, results.total)
136- self.request = request
137- request_start = request.get(self.start_variable_name, None)
138- if request_start is None:
139- self.start = start
140- else:
141- try:
142- self.start = int(request_start)
143- except (ValueError, TypeError):
144- self.start = start
145+ super(GoogleBatchNavigator, self).__init__(results, request,
146+ start=start, size=size, callback=callback,
147+ transient_parameters=transient_parameters, force_start=force_start,
148+ range_factory=range_factory)
149
150+ def determineSize(self, size, batch_params_source):
151+ # Force the default and users requested sizes to 20.
152 self.default_size = 20
153-
154- self.transient_parameters = [self.start_variable_name]
155-
156- self.batch = WindowedListBatch(
157- results, start=self.start, size=self.default_size)
158- self.setHeadings(
159- self.default_singular_heading, self.default_plural_heading)
160+ return 20
161
162=== modified file 'lib/lp/app/browser/tests/launchpad-search-pages.txt'
163--- lib/lp/app/browser/tests/launchpad-search-pages.txt 2011-04-12 11:36:11 +0000
164+++ lib/lp/app/browser/tests/launchpad-search-pages.txt 2011-04-12 20:07:39 +0000
165@@ -444,8 +444,6 @@
166 provides a link to the next batch, which also happens to be the last
167 batch.
168
169- >>> len(search_view.pages.getBatches())
170- 2
171 >>> search_view.pages.nextBatchURL()
172 '...start=20'
173 >>> search_view.pages.lastBatchURL()
174
175=== modified file 'lib/lp/bugs/stories/webservice/xx-bug-tracker.txt'
176--- lib/lp/bugs/stories/webservice/xx-bug-tracker.txt 2011-04-12 11:36:11 +0000
177+++ lib/lp/bugs/stories/webservice/xx-bug-tracker.txt 2011-04-12 20:07:39 +0000
178@@ -10,7 +10,7 @@
179 >>> bug_tracker_collection = anon_webservice.get(
180 ... '/bugs/bugtrackers').jsonBody()
181 >>> pprint_collection(bug_tracker_collection)
182- next_collection_link: u'http://.../bugs/bugtrackers?ws.start=5&ws.size=5'
183+ next_collection_link: u'http://.../bugs/bugtrackers?ws.size=5&memo=5&ws.start=5'
184 resource_type_link: u'http://.../#bug_trackers'
185 start: 0
186 total_size: 8
187
188=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
189--- lib/lp/bugs/stories/webservice/xx-bug.txt 2011-04-12 11:36:11 +0000
190+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2011-04-12 20:07:39 +0000
191@@ -1837,7 +1837,7 @@
192
193 >>> cves = webservice.get("/bugs/cve").jsonBody()
194 >>> pprint_collection(cves)
195- next_collection_link: u'http://.../bugs/cve?ws.start=5&ws.size=5'
196+ next_collection_link: u'http://.../bugs/cve?ws.size=5&memo=5&ws.start=5'
197 resource_type_link: u'http://.../#cves'
198 start: 0
199 total_size: 10
200@@ -2126,7 +2126,7 @@
201 >>> activity = webservice.get(
202 ... bug_one['activity_collection_link']).jsonBody()
203 >>> pprint_collection(activity)
204- next_collection_link: u'http://.../bugs/1/activity?ws.start=5&ws.size=5'
205+ next_collection_link: u'http://.../bugs/1/activity?ws.size=5&memo=5&ws.start=5'
206 resource_type_link: u'http://.../#bug_activity-page-resource'
207 start: 0
208 total_size: 25
209
210=== modified file 'lib/lp/services/features/browser/tests/test_changelog.py'
211--- lib/lp/services/features/browser/tests/test_changelog.py 2011-04-12 11:36:11 +0000
212+++ lib/lp/services/features/browser/tests/test_changelog.py 2011-04-12 20:07:39 +0000
213@@ -80,7 +80,7 @@
214 self.assertEqual('change', batch._singular_heading)
215 self.assertEqual('changes', batch._plural_heading)
216 self.assertEqual(10, batch.default_size)
217- self.assertEqual(2, len(batch.getBatches()))
218+ self.assertEqual(None, batch.currentBatch().nextBatch().nextBatch())
219
220 def test_page_batched_changes(self):
221 self.makeFeatureFlagChanges()
222
223=== modified file 'lib/lp/translations/browser/tests/pofile-views.txt'
224--- lib/lp/translations/browser/tests/pofile-views.txt 2011-04-12 11:36:11 +0000
225+++ lib/lp/translations/browser/tests/pofile-views.txt 2011-04-12 20:07:39 +0000
226@@ -241,7 +241,7 @@
227 Also, we get redirected to the next batch.
228
229 >>> pofile_view.request.response.getHeader('Location')
230- 'http://127.0.0.1?start=10'
231+ 'http://127.0.0.1?memo=10&start=10'
232
233 The messsage's sequence is the position of that message in latest imported
234 template. We are going to test now what happens when we submit a potmsgset
235
236=== modified file 'versions.cfg'
237--- versions.cfg 2011-04-12 11:36:11 +0000
238+++ versions.cfg 2011-04-12 20:07:39 +0000
239@@ -29,7 +29,7 @@
240 keyring = 0.5.1
241 launchpadlib = 1.9.3
242 lazr.authentication = 0.1.1
243-lazr.batchnavigator = 1.2.2
244+lazr.batchnavigator = 1.2.4
245 lazr.config = 1.1.3
246 lazr.delegates = 1.2.0
247 lazr.enum = 1.1.2