Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(337)

Side by Side Diff: appengine/monorail/search/ast2sort.py

Issue 1868553004: Open Source Monorail (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Rebase Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2016 The Chromium Authors. All rights reserved.
2 # Use of this source code is govered by a BSD-style
3 # license that can be found in the LICENSE file or at
4 # https://developers.google.com/open-source/licenses/bsd
5
6 """Convert a user's issue sorting directives into SQL clauses.
7
8 Some sort directives translate into simple ORDER BY column specifications.
9 Other sort directives require that a LEFT JOIN be done to bring in
10 relevant information that is then used in the ORDER BY.
11
12 Sorting based on strings can slow down the DB because long sort-keys
13 must be loaded into RAM, which means that fewer sort-keys fit into the
14 DB's sorting buffers at a time. Also, Monorail defines the sorting
15 order of well-known labels and statuses based on the order in which
16 they are defined in the project's config. So, we determine the sort order of
17 labels and status values before executing the query and then use the MySQL
18 FIELD() function to sort their IDs in the desired order, without sorting
19 strings.
20
21 For more info, see the "Sorting in Monorail" and "What makes Monorail Fast?"
22 design docs.
23 """
24
25 import logging
26
27 from framework import sql
28 from proto import tracker_pb2
29
30
31 NATIVE_SORTABLE_FIELDS = [
32 'id', 'stars', 'attachments', 'opened', 'closed', 'modified']
33
34 FIELDS_TO_COLUMNS = {
35 'id': 'local_id',
36 'stars': 'star_count',
37 'attachments': 'attachment_count',
38 }
39
40
41 def BuildSortClauses(
42 sort_directives, harmonized_labels, harmonized_statuses,
43 harmonized_fields):
44 """Return LEFT JOIN and ORDER BY clauses needed to sort the results."""
45 if not sort_directives:
46 return [], []
47
48 all_left_joins = []
49 all_order_by = []
50 for i, sd in enumerate(sort_directives):
51 left_join_parts, order_by_parts = _OneSortDirective(
52 i, sd, harmonized_labels, harmonized_statuses, harmonized_fields)
53 all_left_joins.extend(left_join_parts)
54 all_order_by.extend(order_by_parts)
55
56 return all_left_joins, all_order_by
57
58
59 def _ProcessProjectSD(fmt):
60 """Convert a 'project' sort directive into SQL."""
61 left_joins = []
62 order_by = [(fmt('Issue.project_id {sort_dir}'), [])]
63 return left_joins, order_by
64
65
66 def _ProcessReporterSD(fmt):
67 """Convert a 'reporter' sort directive into SQL."""
68 left_joins = [
69 (fmt('User AS {alias} ON Issue.reporter_id = {alias}.user_id'), [])]
70 order_by = [
71 (fmt('ISNULL({alias}.email) {sort_dir}'), []),
72 (fmt('{alias}.email {sort_dir}'), [])]
73 return left_joins, order_by
74
75
76 def _ProcessOwnerSD(fmt):
77 """Convert a 'owner' sort directive into SQL."""
78 left_joins = [
79 (fmt('User AS {alias} ON (Issue.owner_id = {alias}.user_id OR '
80 'Issue.derived_owner_id = {alias}.user_id)'), [])]
81 order_by = [
82 (fmt('ISNULL({alias}.email) {sort_dir}'), []),
83 (fmt('{alias}.email {sort_dir}'), [])]
84 return left_joins, order_by
85
86
87 def _ProcessCcSD(fmt):
88 """Convert a 'cc' sort directive into SQL."""
89 # Note: derived cc's are included automatically.
90 # Note: This sorts on the best Cc, not all Cc addresses.
91 # Being more exact might require GROUP BY and GROUP_CONCAT().
92 left_joins = [
93 (fmt('Issue2Cc AS {alias} ON Issue.id = {alias}.issue_id '
94 'LEFT JOIN User AS {alias}_user '
95 'ON {alias}.cc_id = {alias}_user.user_id'), [])]
96 order_by = [
97 (fmt('ISNULL({alias}_user.email) {sort_dir}'), []),
98 (fmt('{alias}_user.email {sort_dir}'), [])]
99 return left_joins, order_by
100
101
102 def _ProcessComponentSD(fmt):
103 """Convert a 'component' sort directive into SQL."""
104 # Note: derived components are included automatically.
105 # Note: This sorts on the best component, not all of them.
106 # Being more exact might require GROUP BY and GROUP_CONCAT().
107 left_joins = [
108 (fmt('Issue2Component AS {alias} ON Issue.id = {alias}.issue_id '
109 'LEFT JOIN ComponentDef AS {alias}_component '
110 'ON {alias}.component_id = {alias}_component.id'), [])]
111 order_by = [
112 (fmt('ISNULL({alias}_component.path) {sort_dir}'), []),
113 (fmt('{alias}_component.path {sort_dir}'), [])]
114 return left_joins, order_by
115
116
117 def _ProcessSummarySD(fmt):
118 """Convert a 'summary' sort directive into SQL."""
119 left_joins = [
120 (fmt('IssueSummary AS {alias} ON Issue.id = {alias}.issue_id'), [])]
121 order_by = [(fmt('{alias}.summary {sort_dir}'), [])]
122 return left_joins, order_by
123
124
125 def _ProcessStatusSD(fmt, harmonized_statuses):
126 """Convert a 'status' sort directive into SQL."""
127 left_joins = []
128 # Note: status_def_rows are already ordered by REVERSED rank.
129 wk_status_ids = [
130 stat_id for stat_id, rank, _ in harmonized_statuses
131 if rank is not None]
132 odd_status_ids = [
133 stat_id for stat_id, rank, _ in harmonized_statuses
134 if rank is None]
135 wk_status_ph = sql.PlaceHolders(wk_status_ids)
136 # Even though oddball statuses sort lexographically, use FIELD to determine
137 # the order so that the database sorts ints rather than strings for speed.
138 odd_status_ph = sql.PlaceHolders(odd_status_ids)
139
140 order_by = [] # appended to below: both well-known and oddball can apply
141 sort_col = ('IF(ISNULL(Issue.status_id), Issue.derived_status_id, '
142 'Issue.status_id)')
143 # Reverse sort by using rev_sort_dir because we want NULLs at the end.
144 if wk_status_ids:
145 order_by.append(
146 (fmt('FIELD({sort_col}, {wk_status_ph}) {rev_sort_dir}',
147 sort_col=sort_col, wk_status_ph=wk_status_ph),
148 wk_status_ids))
149 if odd_status_ids:
150 order_by.append(
151 (fmt('FIELD({sort_col}, {odd_status_ph}) {rev_sort_dir}',
152 sort_col=sort_col, odd_status_ph=odd_status_ph),
153 odd_status_ids))
154
155 return left_joins, order_by
156
157
158 def _ProcessBlockedSD(fmt):
159 """Convert a 'blocked' sort directive into SQL."""
160 left_joins = [
161 (fmt('IssueRelation AS {alias} ON Issue.id = {alias}.issue_id '
162 'AND {alias}.kind = %s'),
163 ['blockedon'])]
164 order_by = [(fmt('ISNULL({alias}.dst_issue_id) {sort_dir}'), [])]
165 return left_joins, order_by
166
167
168 def _ProcessBlockedOnSD(fmt):
169 """Convert a 'blockedon' sort directive into SQL."""
170 left_joins = [
171 (fmt('IssueRelation AS {alias} ON Issue.id = {alias}.issue_id '
172 'AND {alias}.kind = %s'),
173 ['blockedon'])]
174 order_by = [(fmt('ISNULL({alias}.dst_issue_id) {sort_dir}'), []),
175 (fmt('{alias}.dst_issue_id {sort_dir}'), [])]
176 return left_joins, order_by
177
178
179 def _ProcessBlockingSD(fmt):
180 """Convert a 'blocking' sort directive into SQL."""
181 left_joins = [
182 (fmt('IssueRelation AS {alias} ON Issue.id = {alias}.dst_issue_id '
183 'AND {alias}.kind = %s'),
184 ['blockedon'])]
185 order_by = [(fmt('ISNULL({alias}.issue_id) {sort_dir}'), []),
186 (fmt('{alias}.issue_id {sort_dir}'), [])]
187 return left_joins, order_by
188
189
190 def _ProcessCustomAndLabelSD(
191 sd, harmonized_labels, harmonized_fields, alias, sort_dir, fmt):
192 """Convert a label or custom field sort directive into SQL."""
193 left_joins = []
194 order_by = []
195
196 fd_list = [fd for fd in harmonized_fields
197 if fd.field_name.lower() == sd]
198 if fd_list:
199 int_left_joins, int_order_by = _CustomFieldSortClauses(
200 fd_list, tracker_pb2.FieldTypes.INT_TYPE, 'int_value',
201 alias, sort_dir)
202 str_left_joins, str_order_by = _CustomFieldSortClauses(
203 fd_list, tracker_pb2.FieldTypes.STR_TYPE, 'str_value',
204 alias, sort_dir)
205 user_left_joins, user_order_by = _CustomFieldSortClauses(
206 fd_list, tracker_pb2.FieldTypes.USER_TYPE, 'user_id',
207 alias, sort_dir)
208 left_joins.extend(int_left_joins + str_left_joins + user_left_joins)
209 order_by.extend(int_order_by + str_order_by + user_order_by)
210
211 label_left_joinss, label_order_by = _LabelSortClauses(
212 sd, harmonized_labels, fmt)
213 left_joins.extend(label_left_joinss)
214 order_by.extend(label_order_by)
215
216 return left_joins, order_by
217
218
219 def _LabelSortClauses(sd, harmonized_labels, fmt):
220 """Give LEFT JOIN and ORDER BY terms for label sort directives."""
221 # Note: derived labels should work automatically.
222
223 # label_def_rows are already ordered by REVERSED rank.
224 wk_label_ids = [
225 label_id for label_id, rank, label in harmonized_labels
226 if label.lower().startswith('%s-' % sd) and rank is not None]
227 odd_label_ids = [
228 label_id for label_id, rank, label in harmonized_labels
229 if label.lower().startswith('%s-' % sd) and rank is None]
230 all_label_ids = wk_label_ids + odd_label_ids
231
232 if all_label_ids:
233 left_joins = [
234 (fmt('Issue2Label AS {alias} ON Issue.id = {alias}.issue_id '
235 'AND {alias}.label_id IN ({all_label_ph})',
236 all_label_ph=sql.PlaceHolders(all_label_ids)),
237 all_label_ids)]
238 else:
239 left_joins = []
240
241 order_by = []
242 # Reverse sort by using rev_sort_dir because we want NULLs at the end.
243 if wk_label_ids:
244 order_by.append(
245 (fmt('FIELD({alias}.label_id, {wk_label_ph}) {rev_sort_dir}',
246 wk_label_ph=sql.PlaceHolders(wk_label_ids)),
247 wk_label_ids))
248 if odd_label_ids:
249 # Even though oddball labels sort lexographically, use FIELD to determine
250 # the order so that the database sorts ints rather than strings for speed
251 order_by.append(
252 (fmt('FIELD({alias}.label_id, {odd_label_ph}) {rev_sort_dir}',
253 odd_label_ph=sql.PlaceHolders(odd_label_ids)),
254 odd_label_ids))
255
256 return left_joins, order_by
257
258
259 def _CustomFieldSortClauses(
260 fd_list, value_type, value_column, alias, sort_dir):
261 """Give LEFT JOIN and ORDER BY terms for custom fields of the given type."""
262 relevant_fd_list = [fd for fd in fd_list if fd.field_type == value_type]
263 if not relevant_fd_list:
264 return [], []
265
266 field_ids_ph = sql.PlaceHolders(relevant_fd_list)
267 def Fmt(sql_str):
268 return sql_str.format(
269 value_column=value_column, sort_dir=sort_dir,
270 field_ids_ph=field_ids_ph, alias=alias + '_' + value_column)
271
272 left_joins = [
273 (Fmt('Issue2FieldValue AS {alias} ON Issue.id = {alias}.issue_id '
274 'AND {alias}.field_id IN ({field_ids_ph})'),
275 [fd.field_id for fd in relevant_fd_list])]
276
277 if value_type == tracker_pb2.FieldTypes.USER_TYPE:
278 left_joins.append(
279 (Fmt('User AS {alias}_user ON {alias}.user_id = {alias}_user.user_id'),
280 []))
281 order_by = [
282 (Fmt('ISNULL({alias}_user.email) {sort_dir}'), []),
283 (Fmt('{alias}_user.email {sort_dir}'), [])]
284 else:
285 # Unfortunately, this sorts on the best field value, not all of them.
286 order_by = [
287 (Fmt('ISNULL({alias}.{value_column}) {sort_dir}'), []),
288 (Fmt('{alias}.{value_column} {sort_dir}'), [])]
289
290 return left_joins, order_by
291
292
293 _PROCESSORS = {
294 'component': _ProcessComponentSD,
295 'project': _ProcessProjectSD,
296 'reporter': _ProcessReporterSD,
297 'owner': _ProcessOwnerSD,
298 'cc': _ProcessCcSD,
299 'summary': _ProcessSummarySD,
300 'blocked': _ProcessBlockedSD,
301 'blockedon': _ProcessBlockedOnSD,
302 'blocking': _ProcessBlockingSD,
303 }
304
305
306 def _OneSortDirective(
307 i, sd, harmonized_labels, harmonized_statuses, harmonized_fields):
308 """Return SQL clauses to do the sorting for one sort directive."""
309 alias = 'Sort%d' % i
310 if sd.startswith('-'):
311 sort_dir, rev_sort_dir = 'DESC', 'ASC'
312 sd = sd[1:]
313 else:
314 sort_dir, rev_sort_dir = 'ASC', 'DESC'
315
316 def Fmt(sql_str, **kwargs):
317 return sql_str.format(
318 sort_dir=sort_dir, rev_sort_dir=rev_sort_dir, alias=alias,
319 sd=sd, col=FIELDS_TO_COLUMNS.get(sd, sd), **kwargs)
320
321 if sd in NATIVE_SORTABLE_FIELDS:
322 left_joins = []
323 order_by = [(Fmt('Issue.{col} {sort_dir}'), [])]
324 return left_joins, order_by
325
326 elif sd in _PROCESSORS:
327 proc = _PROCESSORS[sd]
328 return proc(Fmt)
329
330 elif sd == 'status':
331 return _ProcessStatusSD(Fmt, harmonized_statuses)
332 else: # otherwise, it must be a field or label, or both
333 return _ProcessCustomAndLabelSD(
334 sd, harmonized_labels, harmonized_fields, alias, sort_dir, Fmt)
OLDNEW
« no previous file with comments | « appengine/monorail/search/ast2select.py ('k') | appengine/monorail/search/backendnonviewable.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698