launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #05050
[Merge] lp:~adeuring/launchpad/bug-739052-10 into lp:launchpad
Abel Deuring has proposed merging lp:~adeuring/launchpad/bug-739052-10 into lp:launchpad with lp:~adeuring/launchpad/bug-739052-9 as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~adeuring/launchpad/bug-739052-10/+merge/76540
This branch fixes a serious problem of StormRangeFactory (hopefully the last one...): It did not work with empty result sets.
We have two "flavours" of empty result sets: Regular Storm ResultSet instances which have no rows, and instances of EmptyResultSet. (The latter can be returned by model code when it is obvious that a query would not return any rows, to avoid a DB query.)
Both variants have of course the "problem" that expressions like resultset[0] raise an IndexError; moreover, EmptyResultSet has no sort order that is usable by StormRangeFactory. This branch makes StormRangeFactory usable for these empty result sets.
test: ./bin/test canonical -vvt test_batching
no lint
--
https://code.launchpad.net/~adeuring/launchpad/bug-739052-10/+merge/76540
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/bug-739052-10 into lp:launchpad.
=== modified file 'lib/canonical/launchpad/webapp/batching.py'
--- lib/canonical/launchpad/webapp/batching.py 2011-09-22 10:12:25 +0000
+++ lib/canonical/launchpad/webapp/batching.py 2011-09-22 10:12:26 +0000
@@ -23,6 +23,7 @@
SQL,
)
from storm.properties import PropertyColumn
+from storm.store import EmptyResultSet
from storm.zope.interfaces import IResultSet
from zope.component import (
adapts,
@@ -294,11 +295,16 @@
else:
self.plain_resultset = resultset
self.error_cb = error_cb
- self.forward_sort_order = self.getOrderBy()
- if self.forward_sort_order is Undef:
- raise StormRangeFactoryError(
- 'StormRangeFactory requires a sorted result set.')
- self.backward_sort_order = self.reverseSortOrder()
+ if not self.empty_resultset:
+ self.forward_sort_order = self.getOrderBy()
+ if self.forward_sort_order is Undef:
+ raise StormRangeFactoryError(
+ 'StormRangeFactory requires a sorted result set.')
+ self.backward_sort_order = self.reverseSortOrder()
+
+ @property
+ def empty_resultset(self):
+ return zope_isinstance(self.plain_resultset, EmptyResultSet)
def getOrderBy(self):
"""Return the order_by expressions of the result set."""
@@ -334,6 +340,8 @@
def getEndpointMemos(self, batch):
"""See `IRangeFactory`."""
plain_slice = batch.sliced_list.shadow_values
+ if len(plain_slice) == 0:
+ return ('', '')
lower = self.getOrderValuesFor(plain_slice[0])
upper = self.getOrderValuesFor(plain_slice[batch.trueSize - 1])
return (
@@ -549,6 +557,8 @@
def getSlice(self, size, endpoint_memo='', forwards=True):
"""See `IRangeFactory`."""
+ if self.empty_resultset:
+ return ShadowedList([], [])
if forwards:
self.resultset.order_by(*self.forward_sort_order)
else:
@@ -585,6 +595,8 @@
# in the result set, so let's use them. Moreover, for SELECT
# DISTINCT queries, each column used for sorting must appear
# in the result.
+ if self.empty_resultset:
+ return 0
columns = [plain_expression(column) for column in self.getOrderBy()]
select = removeSecurityProxy(self.plain_resultset).get_select_expr(
*columns)
=== modified file 'lib/canonical/launchpad/webapp/tests/test_batching.py'
--- lib/canonical/launchpad/webapp/tests/test_batching.py 2011-09-22 10:12:25 +0000
+++ lib/canonical/launchpad/webapp/tests/test_batching.py 2011-09-22 10:12:26 +0000
@@ -12,6 +12,7 @@
compile,
Desc,
)
+from storm.store import EmptyResultSet
from testtools.matchers import (
LessThan,
Not,
@@ -785,3 +786,45 @@
self.assertIsInstance(sliced, ShadowedList)
self.assertEqual(all_results[2:4], list(sliced))
self.assertEqual(all_undecorated_results[2:4], sliced.shadow_values)
+
+ def test_StormRangeFactory__EmptyResultSet(self):
+ # It is possible to create StormRangeFactory instances for
+ # EmptyResultSets,
+ resultset = EmptyResultSet()
+ range_factory = StormRangeFactory(resultset)
+ # rough_length is always zero.
+ self.assertEqual(0, range_factory.rough_length)
+ # getSlice() and getSliceByIndex() return empty ShadowedLists.
+ sliced = range_factory.getSlice(1)
+ self.assertEqual([], sliced.values)
+ self.assertEqual([], sliced.shadow_values)
+ sliced = range_factory.getSliceByIndex(0, 1)
+ self.assertEqual([], sliced.values)
+ self.assertEqual([], sliced.shadow_values)
+ # The endpoint memos are empty strings.
+ request = LaunchpadTestRequest()
+ batchnav = BatchNavigator(
+ resultset, request, size=3, range_factory=range_factory)
+ first, last = range_factory.getEndpointMemos(batchnav.batch)
+ self.assertEqual('', first)
+ self.assertEqual('', last)
+
+ def test_StormRangeFactory__empty_real_resultset(self):
+ # StormRangeFactory works with empty regular result sets,
+ resultset = self.factory.makeProduct().open_bugtasks
+ self.assertEqual(0, resultset.count())
+ range_factory = StormRangeFactory(resultset)
+ # getSlice() and getSliceByIndex() return empty ShadowedLists.
+ sliced = range_factory.getSlice(1)
+ self.assertEqual([], sliced.values)
+ self.assertEqual([], sliced.shadow_values)
+ sliced = range_factory.getSliceByIndex(0, 1)
+ self.assertEqual([], sliced.values)
+ self.assertEqual([], sliced.shadow_values)
+ # The endpoint memos are empty strings.
+ request = LaunchpadTestRequest()
+ batchnav = BatchNavigator(
+ resultset, request, size=3, range_factory=range_factory)
+ first, last = range_factory.getEndpointMemos(batchnav.batch)
+ self.assertEqual('', first)
+ self.assertEqual('', last)