Merge lp:~adeuring/launchpad/bug-909318 into lp:launchpad

Proposed by Abel Deuring on 2012-01-03
Status: Merged
Approved by: Abel Deuring on 2012-01-03
Approved revision: no longer in the source branch.
Merged at revision: 14629
Proposed branch: lp:~adeuring/launchpad/bug-909318
Merge into: lp:launchpad
Diff against target: 155 lines (+42/-21)
2 files modified
lib/lp/bugs/model/bugtask.py (+16/-14)
lib/lp/bugs/tests/test_bugtask_search.py (+26/-7)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-909318
Reviewer Review Type Date Requested Status
Graham Binns (community) code 2012-01-03 Approve on 2012-01-03
Review via email: mp+87322@code.launchpad.net

Commit Message

[r=gmb][bug=909318] bug fix: invalid SQL query when a bugtask search consisting of a UNION of two or more sub-.queries is ordered by a value that requires to join another table.

Description of the Change

This branch fixes bug 909318: Can’t order bugs by reporter.

The cause is a bug in the SQL query generated by BugTaskSet._search()
when the *args passed to _search are not empty (i.e., when the query
is a UNION of several SELECTs), and when the ordering of the result
set requires to JOIN another table. In this case, the query looked
roughly like:

SELECT BugTask.* FROM (
   SELECT BugTask.* JOIN SortTable ON ... WHERE condition1
   UNION SELECT BugTask.* JOIN SortTable ON ... WHERE condition2
   UNION ...)
   ORDER BY SortTable.sort_column;

Joining the sort table in the different term of the UNION operator
is obviously nonsense.

The expression "JOIN SortTable ON ..." was added in
BugTaskSet.buildQuery() (hunk @@ -2368,9 +2368,6 @@ of the diff).

I moved this to BugTaskSet._search(): BugTaskSet._processOrderBy()
returns (orderby_arg, extra_joins), where extra_joins is a sequence
of tuples (join_table, join_expression), as can be passed to
BugTaskSet.buildOrigin(). So we can conveniently decide in _search()
to which SELECT the ordering related JOINs are added.

tests:

./bin/test bugs -vvt test_bugtask_search.*test_two_param_objects -vvt test_bugtask_search.*test_sort_by

Demo/QA: Visit the bug page of any person/team and sort the
bugs by assignee, reporter or milestone.

no lint

To post a comment you must log in.
Graham Binns (gmb) wrote :

139 + def test_two_param_objects_sorting_needs_extra_join(self):

This needs a comment stating the expected behaviour you're testing for.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/model/bugtask.py'
2--- lib/lp/bugs/model/bugtask.py 2011-12-30 06:14:56 +0000
3+++ lib/lp/bugs/model/bugtask.py 2012-01-04 08:48:29 +0000
4@@ -2368,9 +2368,6 @@
5 "BugTask.datecreated > %s" % (
6 sqlvalues(params.created_since,)))
7
8- orderby_arg, extra_joins = self._processOrderBy(params)
9- join_tables.extend(extra_joins)
10-
11 query = " AND ".join(extra_clauses)
12
13 if not decorators:
14@@ -2386,7 +2383,7 @@
15 else:
16 with_clause = None
17 return (
18- query, clauseTables, orderby_arg, decorator, join_tables,
19+ query, clauseTables, decorator, join_tables,
20 has_duplicate_results, with_clause)
21
22 def buildUpstreamClause(self, params):
23@@ -2644,7 +2641,8 @@
24 SpecificationBug.specification %s)
25 """ % search_value_to_where_condition(linked_blueprints)
26
27- def buildOrigin(self, join_tables, prejoin_tables, clauseTables):
28+ def buildOrigin(self, join_tables, prejoin_tables, clauseTables,
29+ start_with=BugTask):
30 """Build the parameter list for Store.using().
31
32 :param join_tables: A sequence of tables that should be joined
33@@ -2663,7 +2661,7 @@
34 and in clauseTables. This method ensures that each table
35 appears exactly once in the returned sequence.
36 """
37- origin = [BugTask]
38+ origin = [start_with]
39 already_joined = set(origin)
40 for table, join in join_tables:
41 if table is None or table not in already_joined:
42@@ -2691,26 +2689,29 @@
43 :param args: optional additional BugTaskSearchParams instances,
44 """
45 orig_store = store = IStore(BugTask)
46- [query, clauseTables, orderby, bugtask_decorator, join_tables,
47+ [query, clauseTables, bugtask_decorator, join_tables,
48 has_duplicate_results, with_clause] = self.buildQuery(params)
49 if with_clause:
50 store = store.with_(with_clause)
51+ orderby_expression, orderby_joins = self._processOrderBy(params)
52 if len(args) == 0:
53 if has_duplicate_results:
54 origin = self.buildOrigin(join_tables, [], clauseTables)
55- outer_origin = self.buildOrigin([], prejoins, [])
56+ outer_origin = self.buildOrigin(
57+ orderby_joins, prejoins, [])
58 subquery = Select(BugTask.id, where=SQL(query), tables=origin)
59 resultset = store.using(*outer_origin).find(
60 resultrow, In(BugTask.id, subquery))
61 else:
62- origin = self.buildOrigin(join_tables, prejoins, clauseTables)
63+ origin = self.buildOrigin(
64+ join_tables + orderby_joins, prejoins, clauseTables)
65 resultset = store.using(*origin).find(resultrow, query)
66 if prejoins:
67 decorator = lambda row: bugtask_decorator(row[0])
68 else:
69 decorator = bugtask_decorator
70
71- resultset.order_by(orderby)
72+ resultset.order_by(orderby_expression)
73 return DecoratedResultSet(resultset, result_decorator=decorator,
74 pre_iter_hook=pre_iter_hook)
75
76@@ -2720,7 +2721,7 @@
77
78 decorators = [bugtask_decorator]
79 for arg in args:
80- [query, clauseTables, ignore, decorator, join_tables,
81+ [query, clauseTables, decorator, join_tables,
82 has_duplicate_results, with_clause] = self.buildQuery(arg)
83 origin = self.buildOrigin(join_tables, [], clauseTables)
84 localstore = store
85@@ -2745,15 +2746,16 @@
86 bugtask = decorator(bugtask)
87 return bugtask
88
89- origin = [Alias(resultset._get_select(), "BugTask")]
90+ origin = self.buildOrigin(
91+ orderby_joins, prejoins, [],
92+ start_with=Alias(resultset._get_select(), "BugTask"))
93 if prejoins:
94- origin += [join for table, join in prejoins]
95 decorator = prejoin_decorator
96 else:
97 decorator = simple_decorator
98
99 result = store.using(*origin).find(resultrow)
100- result.order_by(orderby)
101+ result.order_by(orderby_expression)
102 return DecoratedResultSet(result, result_decorator=decorator,
103 pre_iter_hook=pre_iter_hook)
104
105
106=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
107--- lib/lp/bugs/tests/test_bugtask_search.py 2012-01-01 02:58:52 +0000
108+++ lib/lp/bugs/tests/test_bugtask_search.py 2012-01-04 08:48:29 +0000
109@@ -1339,20 +1339,39 @@
110 accepts exactly one instance.
111 """
112
113+ def setUpTwoSearchParams(self, orderby=None):
114+ # Prepare the test data for the tests in this class.
115+ params1 = self.getBugTaskSearchParams(
116+ user=None, status=BugTaskStatus.FIXCOMMITTED, orderby=orderby)
117+ subscriber = self.factory.makePerson()
118+ self.subscribeToTarget(subscriber)
119+ params2 = self.getBugTaskSearchParams(
120+ user=None, status=BugTaskStatus.NEW,
121+ structural_subscriber=subscriber, orderby=orderby)
122+ return params1, params2
123+
124 def test_two_param_objects(self):
125 # We can pass more than one BugTaskSearchParams instance to
126 # BugTaskSet.search().
127- params1 = self.getBugTaskSearchParams(
128- user=None, status=BugTaskStatus.FIXCOMMITTED)
129- subscriber = self.factory.makePerson()
130- self.subscribeToTarget(subscriber)
131- params2 = self.getBugTaskSearchParams(
132- user=None, status=BugTaskStatus.NEW,
133- structural_subscriber=subscriber)
134+ params1, params2 = self.setUpTwoSearchParams()
135 search_result = self.runSearch(params1, params2)
136 expected = self.resultValuesForBugtasks(self.bugtasks[1:])
137 self.assertEqual(expected, search_result)
138
139+ def test_two_param_objects_sorting_needs_extra_join(self):
140+ # If result ordering needs an extra join, the join
141+ # is added to the union of the result sets for the two
142+ # BugTaskSearchParams instances.
143+ params1, params2 = self.setUpTwoSearchParams(orderby='reporter')
144+ search_result = self.runSearch(params1, params2)
145+
146+ def sortkey(bugtask):
147+ return bugtask.owner.name
148+
149+ expected_bugtasks = sorted(self.bugtasks[1:], key=sortkey)
150+ expected = self.resultValuesForBugtasks(expected_bugtasks)
151+ self.assertEqual(expected, search_result)
152+
153
154 class PreloadBugtaskTargets(MultipleParams):
155 """Preload bug targets during a BugTaskSet.search() query."""