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

Side by Side Diff: appengine/monorail/search/ast2select.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
« no previous file with comments | « appengine/monorail/search/ast2ast.py ('k') | appengine/monorail/search/ast2sort.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 search AST into SQL clauses.
7
8 The main query is done on the Issues table.
9 + Some simple conditions are implemented as WHERE conditions on the Issue
10 table rows. These are generated by the _Compare() function.
11 + More complex conditions are implemented via a "LEFT JOIN ... ON ..." clause
12 plus a check in the WHERE clause to select only rows where the join's ON
13 condition was satisfied. These are generated by appending a clause to
14 the left_joins list plus calling _CompareAlreadyJoined(). Each such left
15 join defines a unique alias to keep it separate from other conditions.
16
17 The functions that generate SQL snippets need to insert table names, column
18 names, alias names, and value placeholders into the generated string. These
19 functions use the string format() method and the "{varname}" syntax to avoid
20 confusion with the "%s" syntax used for SQL value placeholders.
21 """
22
23 import logging
24
25 from framework import sql
26 from proto import ast_pb2
27 from proto import tracker_pb2
28 from services import tracker_fulltext
29
30
31 NATIVE_SEARCHABLE_FIELDS = {
32 'id': 'local_id',
33 'stars': 'star_count',
34 'attachments': 'attachment_count',
35 'opened': 'opened',
36 'closed': 'closed',
37 'modified': 'modified',
38 'spam': 'is_spam'
39 }
40
41
42 def BuildSQLQuery(query_ast):
43 """Translate the user's query into an SQL query.
44
45 Args:
46 query_ast: user query abstract syntax tree parsed by query2ast.py.
47
48 Returns:
49 A pair of lists (left_joins, where) to use when building the SQL SELECT
50 statement. Each of them is a list of (str, [val, ...]) pairs.
51 """
52 left_joins = []
53 where = []
54 # TODO(jrobbins): Handle "OR" in queries. For now, we just process the
55 # first conjunction and assume that it is the only one.
56 assert len(query_ast.conjunctions) == 1, 'TODO(jrobbins) handle "OR" queries'
57 conj = query_ast.conjunctions[0]
58
59 for cond_num, cond in enumerate(conj.conds):
60 cond_left_joins, cond_where = _ProcessCond(cond_num, cond)
61 left_joins.extend(cond_left_joins)
62 where.extend(cond_where)
63
64 return left_joins, where
65
66
67 def _ProcessBlockedOnIDCond(cond, alias, _user_alias):
68 """Convert a blockedon_id=issue_id cond to SQL."""
69 return _GetBlockIDCond(cond, alias, blocking_id=False)
70
71
72 def _ProcessBlockingIDCond(cond, alias, _user_alias):
73 """Convert a blocking_id:1,2 cond to SQL."""
74 return _GetBlockIDCond(cond, alias, blocking_id=True)
75
76
77 def _GetBlockIDCond(cond, alias, blocking_id=False):
78 """Convert either a blocking_id or blockedon_id cond to SQL.
79
80 If blocking_id is False then it is treated as a blockedon_id request,
81 otherwise it is treated as a blocking_id request.
82 """
83 matching_issue_col = 'issue_id' if blocking_id else 'dst_issue_id'
84 ret_issue_col = 'dst_issue_id' if blocking_id else 'issue_id'
85
86 kind_cond_str, kind_cond_args = _Compare(
87 alias, ast_pb2.QueryOp.EQ, tracker_pb2.FieldTypes.STR_TYPE, 'kind',
88 ['blockedon'])
89 left_joins = [(
90 ('IssueRelation AS {alias} ON Issue.id = {alias}.%s AND '
91 '{kind_cond}' % ret_issue_col).format(
92 alias=alias, kind_cond=kind_cond_str), kind_cond_args)]
93
94 field_type, field_values = _GetFieldTypeAndValues(cond)
95 if field_values:
96 where = [_Compare(
97 alias, ast_pb2.QueryOp.EQ, field_type, matching_issue_col,
98 field_values)]
99 else:
100 # If no field values are specified display all issues which have the
101 # property.
102 where = [_CompareAlreadyJoined(alias, cond.op, ret_issue_col)]
103
104 return left_joins, where
105
106
107 def _GetFieldTypeAndValues(cond):
108 """Returns the field type and values to use from the condition.
109
110 This function should be used when we do not know what values are present on
111 the condition. Eg: cond.int_values could be set if ast2ast.py preprocessing is
112 first done. If that preprocessing is not done then str_values could be set
113 instead.
114 If both int values and str values exist on the condition then the int values
115 are returned.
116 """
117 if cond.int_values:
118 return tracker_pb2.FieldTypes.INT_TYPE, cond.int_values
119 else:
120 return tracker_pb2.FieldTypes.STR_TYPE, cond.str_values
121
122
123 def _ProcessOwnerCond(cond, alias, _user_alias):
124 """Convert an owner:substring cond to SQL."""
125 left_joins = [(
126 'User AS {alias} ON (Issue.owner_id = {alias}.user_id '
127 'OR Issue.derived_owner_id = {alias}.user_id)'.format(
128 alias=alias), [])]
129 where = [_Compare(alias, cond.op, tracker_pb2.FieldTypes.STR_TYPE, 'email',
130 cond.str_values)]
131
132 return left_joins, where
133
134
135 def _ProcessOwnerIDCond(cond, _alias, _user_alias):
136 """Convert an owner_id=user_id cond to SQL."""
137 field_type, field_values = _GetFieldTypeAndValues(cond)
138 explicit_str, explicit_args = _Compare(
139 'Issue', cond.op, field_type, 'owner_id', field_values)
140 derived_str, derived_args = _Compare(
141 'Issue', cond.op, field_type, 'derived_owner_id', field_values)
142 if cond.op in (ast_pb2.QueryOp.NE, ast_pb2.QueryOp.NOT_TEXT_HAS):
143 where = [(explicit_str, explicit_args), (derived_str, derived_args)]
144 else:
145 if cond.op == ast_pb2.QueryOp.IS_NOT_DEFINED:
146 op = ' AND '
147 else:
148 op = ' OR '
149 where = [
150 ('(' + explicit_str + op + derived_str + ')',
151 explicit_args + derived_args)]
152
153 return [], where
154
155
156 def _ProcessReporterCond(cond, alias, _user_alias):
157 """Convert a reporter:substring cond to SQL."""
158 left_joins = [(
159 'User AS {alias} ON Issue.reporter_id = {alias}.user_id'.format(
160 alias=alias), [])]
161 where = [_Compare(alias, cond.op, tracker_pb2.FieldTypes.STR_TYPE, 'email',
162 cond.str_values)]
163
164 return left_joins, where
165
166
167 def _ProcessReporterIDCond(cond, _alias, _user_alias):
168 """Convert a reporter_ID=user_id cond to SQL."""
169 field_type, field_values = _GetFieldTypeAndValues(cond)
170 where = [_Compare(
171 'Issue', cond.op, field_type, 'reporter_id', field_values)]
172 return [], where
173
174
175 def _ProcessCcCond(cond, alias, user_alias):
176 """Convert a cc:substring cond to SQL."""
177 email_cond_str, email_cond_args = _Compare(
178 user_alias, cond.op, tracker_pb2.FieldTypes.STR_TYPE, 'email',
179 cond.str_values)
180 # Note: email_cond_str will have parens, if needed.
181 left_joins = [(
182 '(Issue2Cc AS {alias} JOIN User AS {user_alias} '
183 'ON {alias}.cc_id = {user_alias}.user_id AND {email_cond}) '
184 'ON Issue.id = {alias}.issue_id AND '
185 'Issue.shard = {alias}.issue_shard'.format(
186 alias=alias, user_alias=user_alias, email_cond=email_cond_str),
187 email_cond_args)]
188 where = [_CompareAlreadyJoined(user_alias, cond.op, 'email')]
189
190 return left_joins, where
191
192
193 def _ProcessCcIDCond(cond, alias, _user_alias):
194 """Convert a cc_id=user_id cond to SQL."""
195 join_str = (
196 'Issue2Cc AS {alias} ON Issue.id = {alias}.issue_id AND '
197 'Issue.shard = {alias}.issue_shard'.format(
198 alias=alias))
199 if cond.op in (ast_pb2.QueryOp.IS_DEFINED, ast_pb2.QueryOp.IS_NOT_DEFINED):
200 left_joins = [(join_str, [])]
201 else:
202 field_type, field_values = _GetFieldTypeAndValues(cond)
203 cond_str, cond_args = _Compare(
204 alias, ast_pb2.QueryOp.EQ, field_type, 'cc_id', field_values)
205 left_joins = [(join_str + ' AND ' + cond_str, cond_args)]
206
207 where = [_CompareAlreadyJoined(alias, cond.op, 'cc_id')]
208 return left_joins, where
209
210
211 def _ProcessStarredByCond(cond, alias, user_alias):
212 """Convert a starredby:substring cond to SQL."""
213 email_cond_str, email_cond_args = _Compare(
214 user_alias, cond.op, tracker_pb2.FieldTypes.STR_TYPE, 'email',
215 cond.str_values)
216 # Note: email_cond_str will have parens, if needed.
217 left_joins = [(
218 '(IssueStar AS {alias} JOIN User AS {user_alias} '
219 'ON {alias}.user_id = {user_alias}.user_id AND {email_cond}) '
220 'ON Issue.id = {alias}.issue_id'.format(
221 alias=alias, user_alias=user_alias, email_cond=email_cond_str),
222 email_cond_args)]
223 where = [_CompareAlreadyJoined(user_alias, cond.op, 'email')]
224
225 return left_joins, where
226
227
228 def _ProcessStarredByIDCond(cond, alias, _user_alias):
229 """Convert a starredby_id=user_id cond to SQL."""
230 join_str = 'IssueStar AS {alias} ON Issue.id = {alias}.issue_id'.format(
231 alias=alias)
232 if cond.op in (ast_pb2.QueryOp.IS_DEFINED, ast_pb2.QueryOp.IS_NOT_DEFINED):
233 left_joins = [(join_str, [])]
234 else:
235 field_type, field_values = _GetFieldTypeAndValues(cond)
236 cond_str, cond_args = _Compare(
237 alias, ast_pb2.QueryOp.EQ, field_type, 'user_id', field_values)
238 left_joins = [(join_str + ' AND ' + cond_str, cond_args)]
239
240 where = [_CompareAlreadyJoined(alias, cond.op, 'user_id')]
241 return left_joins, where
242
243
244 def _ProcessCommentByCond(cond, alias, user_alias):
245 """Convert a commentby:substring cond to SQL."""
246 email_cond_str, email_cond_args = _Compare(
247 user_alias, cond.op, tracker_pb2.FieldTypes.STR_TYPE, 'email',
248 cond.str_values)
249 # Note: email_cond_str will have parens, if needed.
250 left_joins = [(
251 '(Comment AS {alias} JOIN User AS {user_alias} '
252 'ON {alias}.commenter_id = {user_alias}.user_id AND {email_cond}) '
253 'ON Issue.id = {alias}.issue_id'.format(
254 alias=alias, user_alias=user_alias, email_cond=email_cond_str),
255 email_cond_args)]
256 where = [_CompareAlreadyJoined(user_alias, cond.op, 'email')]
257
258 return left_joins, where
259
260
261 def _ProcessCommentByIDCond(cond, alias, _user_alias):
262 """Convert a commentby_id=user_id cond to SQL."""
263 left_joins = [(
264 'Comment AS {alias} ON Issue.id = {alias}.issue_id'.format(
265 alias=alias), [])]
266 if cond.op in (ast_pb2.QueryOp.IS_DEFINED, ast_pb2.QueryOp.IS_NOT_DEFINED):
267 where = [_CompareAlreadyJoined(alias, cond.op, 'commenter_id')]
268 else:
269 field_type, field_values = _GetFieldTypeAndValues(cond)
270 where = [_Compare(alias, cond.op, field_type, 'commenter_id', field_values)]
271
272 return left_joins, where
273
274
275 def _ProcessStatusIDCond(cond, _alias, _user_alias):
276 """Convert a status_id=ID cond to SQL."""
277 field_type, field_values = _GetFieldTypeAndValues(cond)
278 explicit_str, explicit_args = _Compare(
279 'Issue', cond.op, field_type, 'status_id', field_values)
280 derived_str, derived_args = _Compare(
281 'Issue', cond.op, field_type, 'derived_status_id', field_values)
282 if cond.op in (ast_pb2.QueryOp.IS_NOT_DEFINED, ast_pb2.QueryOp.NE):
283 where = [(explicit_str, explicit_args), (derived_str, derived_args)]
284 else:
285 where = [
286 ('(' + explicit_str + ' OR ' + derived_str + ')',
287 explicit_args + derived_args)]
288
289 return [], where
290
291
292 def _ProcessLabelIDCond(cond, alias, _user_alias):
293 """Convert a label_id=ID cond to SQL."""
294 join_str = (
295 'Issue2Label AS {alias} ON Issue.id = {alias}.issue_id AND '
296 'Issue.shard = {alias}.issue_shard'.format(alias=alias))
297 field_type, field_values = _GetFieldTypeAndValues(cond)
298 cond_str, cond_args = _Compare(
299 alias, ast_pb2.QueryOp.EQ, field_type, 'label_id', field_values)
300 left_joins = [(join_str + ' AND ' + cond_str, cond_args)]
301 where = [_CompareAlreadyJoined(alias, cond.op, 'label_id')]
302 return left_joins, where
303
304
305 def _ProcessComponentIDCond(cond, alias, _user_alias):
306 """Convert a component_id=ID cond to SQL."""
307 # This is a built-in field, so it shadows any other fields w/ the same name.
308 join_str = (
309 'Issue2Component AS {alias} ON Issue.id = {alias}.issue_id AND '
310 'Issue.shard = {alias}.issue_shard'.format(alias=alias))
311 if cond.op in (ast_pb2.QueryOp.IS_DEFINED, ast_pb2.QueryOp.IS_NOT_DEFINED):
312 left_joins = [(join_str, [])]
313 else:
314 field_type, field_values = _GetFieldTypeAndValues(cond)
315 cond_str, cond_args = _Compare(
316 alias, ast_pb2.QueryOp.EQ, field_type, 'component_id', field_values)
317 left_joins = [(join_str + ' AND ' + cond_str, cond_args)]
318
319 where = [_CompareAlreadyJoined(alias, cond.op, 'component_id')]
320 return left_joins, where
321
322
323 def _ProcessCustomFieldCond(cond, alias, user_alias):
324 """Convert a custom field cond to SQL."""
325 # TODO(jrobbins): handle ambiguous field names that map to multiple
326 # field definitions, especially for cross-project search.
327 field_def = cond.field_defs[0]
328 val_type = field_def.field_type
329
330 join_str = (
331 'Issue2FieldValue AS {alias} ON Issue.id = {alias}.issue_id AND '
332 'Issue.shard = {alias}.issue_shard AND '
333 '{alias}.field_id = %s'.format(alias=alias))
334 left_joins = [(join_str, [field_def.field_id])]
335 if val_type == tracker_pb2.FieldTypes.INT_TYPE:
336 where = [_Compare(alias, cond.op, val_type, 'int_value', cond.int_values)]
337 elif val_type == tracker_pb2.FieldTypes.STR_TYPE:
338 where = [_Compare(alias, cond.op, val_type, 'str_value', cond.str_values)]
339 elif val_type == tracker_pb2.FieldTypes.USER_TYPE:
340 if cond.int_values or cond.op in (
341 ast_pb2.QueryOp.IS_DEFINED, ast_pb2.QueryOp.IS_NOT_DEFINED):
342 where = [_Compare(alias, cond.op, val_type, 'user_id', cond.int_values)]
343 else:
344 email_cond_str, email_cond_args = _Compare(
345 user_alias, cond.op, val_type, 'email', cond.str_values)
346 left_joins.append((
347 'User AS {user_alias} ON {alias}.user_id = {user_alias}.user_id '
348 'AND {email_cond}'.format(
349 alias=alias, user_alias=user_alias, email_cond=email_cond_str),
350 email_cond_args))
351 where = [_CompareAlreadyJoined(user_alias, cond.op, 'email')]
352
353 return left_joins, where
354
355
356 def _ProcessAttachmentCond(cond, alias, _user_alias):
357 """Convert has:attachment and -has:attachment cond to SQL."""
358 if cond.op in (ast_pb2.QueryOp.IS_DEFINED, ast_pb2.QueryOp.IS_NOT_DEFINED):
359 left_joins = []
360 where = [_Compare('Issue', cond.op, tracker_pb2.FieldTypes.INT_TYPE,
361 'attachment_count', cond.int_values)]
362 else:
363 field_def = cond.field_defs[0]
364 val_type = field_def.field_type
365 left_joins = [
366 ('Attachment AS {alias} ON Issue.id = {alias}.issue_id AND '
367 '{alias}.deleted = %s'.format(alias=alias),
368 [False])]
369 where = [_Compare(alias, cond.op, val_type, 'filename', cond.str_values)]
370
371 return left_joins, where
372
373
374 _PROCESSORS = {
375 'owner': _ProcessOwnerCond,
376 'owner_id': _ProcessOwnerIDCond,
377 'reporter': _ProcessReporterCond,
378 'reporter_id': _ProcessReporterIDCond,
379 'cc': _ProcessCcCond,
380 'cc_id': _ProcessCcIDCond,
381 'starredby': _ProcessStarredByCond,
382 'starredby_id': _ProcessStarredByIDCond,
383 'commentby': _ProcessCommentByCond,
384 'commentby_id': _ProcessCommentByIDCond,
385 'status_id': _ProcessStatusIDCond,
386 'label_id': _ProcessLabelIDCond,
387 'component_id': _ProcessComponentIDCond,
388 'blockedon_id': _ProcessBlockedOnIDCond,
389 'blocking_id': _ProcessBlockingIDCond,
390 'attachment': _ProcessAttachmentCond,
391 }
392
393
394 def _ProcessCond(cond_num, cond):
395 """Translate one term of the user's search into an SQL query.
396
397 Args:
398 cond_num: integer cond number used to make distinct local variable names.
399 cond: user query cond parsed by query2ast.py.
400
401 Returns:
402 A pair of lists (left_joins, where) to use when building the SQL SELECT
403 statement. Each of them is a list of (str, [val, ...]) pairs.
404 """
405 alias = 'Cond%d' % cond_num
406 user_alias = 'User%d' % cond_num
407 # Note: a condition like [x=y] has field_name "x", there may be multiple
408 # field definitions that match "x", but they will all have field_name "x".
409 field_def = cond.field_defs[0]
410 assert all(field_def.field_name == fd.field_name for fd in cond.field_defs)
411
412 if field_def.field_name in NATIVE_SEARCHABLE_FIELDS:
413 col = NATIVE_SEARCHABLE_FIELDS[field_def.field_name]
414 where = [_Compare(
415 'Issue', cond.op, field_def.field_type, col,
416 cond.str_values or cond.int_values)]
417 return [], where
418
419 elif field_def.field_name in _PROCESSORS:
420 proc = _PROCESSORS[field_def.field_name]
421 return proc(cond, alias, user_alias)
422
423 elif field_def.field_id: # it is a search on a custom field
424 return _ProcessCustomFieldCond(cond, alias, user_alias)
425
426 elif (field_def.field_name in tracker_fulltext.ISSUE_FULLTEXT_FIELDS or
427 field_def.field_name == 'any_field'):
428 pass # handled by full-text search.
429
430 else:
431 logging.error('untranslated search cond %r', cond)
432
433 return [], []
434
435
436 def _Compare(alias, op, val_type, col, vals):
437 """Return an SQL comparison for the given values. For use in WHERE or ON.
438
439 Args:
440 alias: String name of the table or alias defined in a JOIN clause.
441 op: One of the operators defined in ast_pb2.py.
442 val_type: One of the value types defined in ast_pb2.py.
443 col: string column name to compare to vals.
444 vals: list of values that the user is searching for.
445
446 Returns:
447 (cond_str, cond_args) where cond_str is a SQL condition that may contain
448 some %s placeholders, and cond_args is the list of values that fill those
449 placeholders. If the condition string contains any AND or OR operators,
450 the whole expression is put inside parens.
451
452 Raises:
453 NoPossibleResults: The user's query is impossible to ever satisfy, e.g.,
454 it requires matching an empty set of labels.
455 """
456 vals_ph = sql.PlaceHolders(vals)
457 if col in ['label', 'status', 'email']:
458 alias_col = 'LOWER(%s.%s)' % (alias, col)
459 else:
460 alias_col = '%s.%s' % (alias, col)
461
462 def Fmt(cond_str):
463 return cond_str.format(alias_col=alias_col, vals_ph=vals_ph)
464
465 no_value = (0 if val_type in [tracker_pb2.FieldTypes.DATE_TYPE,
466 tracker_pb2.FieldTypes.INT_TYPE] else '')
467 if op == ast_pb2.QueryOp.IS_DEFINED:
468 return Fmt('({alias_col} IS NOT NULL AND {alias_col} != %s)'), [no_value]
469 if op == ast_pb2.QueryOp.IS_NOT_DEFINED:
470 return Fmt('({alias_col} IS NULL OR {alias_col} = %s)'), [no_value]
471
472 if val_type in [tracker_pb2.FieldTypes.DATE_TYPE,
473 tracker_pb2.FieldTypes.INT_TYPE]:
474 if op == ast_pb2.QueryOp.TEXT_HAS:
475 op = ast_pb2.QueryOp.EQ
476 if op == ast_pb2.QueryOp.NOT_TEXT_HAS:
477 op = ast_pb2.QueryOp.NE
478
479 if op == ast_pb2.QueryOp.EQ:
480 if not vals:
481 raise NoPossibleResults('Column %s has no possible value' % alias_col)
482 elif len(vals) == 1:
483 cond_str = Fmt('{alias_col} = %s')
484 else:
485 cond_str = Fmt('{alias_col} IN ({vals_ph})')
486 return cond_str, vals
487
488 if op == ast_pb2.QueryOp.NE:
489 if not vals:
490 return 'TRUE', [] # a no-op that matches every row.
491 elif len(vals) == 1:
492 comp = Fmt('{alias_col} != %s')
493 else:
494 comp = Fmt('{alias_col} NOT IN ({vals_ph})')
495 return '(%s IS NULL OR %s)' % (alias_col, comp), vals
496
497 # Note: These operators do not support quick-OR
498 val = vals[0]
499
500 if op == ast_pb2.QueryOp.GT:
501 return Fmt('{alias_col} > %s'), [val]
502 if op == ast_pb2.QueryOp.LT:
503 return Fmt('{alias_col} < %s'), [val]
504 if op == ast_pb2.QueryOp.GE:
505 return Fmt('{alias_col} >= %s'), [val]
506 if op == ast_pb2.QueryOp.LE:
507 return Fmt('{alias_col} <= %s'), [val]
508
509 if op == ast_pb2.QueryOp.TEXT_MATCHES:
510 return Fmt('{alias_col} LIKE %s'), [val]
511 if op == ast_pb2.QueryOp.NOT_TEXT_MATCHES:
512 return Fmt('({alias_col} IS NULL OR {alias_col} NOT LIKE %s)'), [val]
513
514 if op == ast_pb2.QueryOp.TEXT_HAS:
515 return Fmt('{alias_col} LIKE %s'), ['%' + val + '%']
516 if op == ast_pb2.QueryOp.NOT_TEXT_HAS:
517 return (Fmt('({alias_col} IS NULL OR {alias_col} NOT LIKE %s)'),
518 ['%' + val + '%'])
519
520 logging.error('unknown op: %r', op)
521
522
523 def _CompareAlreadyJoined(alias, op, col):
524 """Return a WHERE clause comparison that checks that a join succeeded."""
525 def Fmt(cond_str):
526 return cond_str.format(alias_col='%s.%s' % (alias, col))
527
528 if op in (ast_pb2.QueryOp.EQ, ast_pb2.QueryOp.TEXT_HAS,
529 ast_pb2.QueryOp.TEXT_MATCHES, ast_pb2.QueryOp.IS_DEFINED):
530 return Fmt('{alias_col} IS NOT NULL'), []
531
532 if op in (ast_pb2.QueryOp.NE, ast_pb2.QueryOp.NOT_TEXT_HAS,
533 ast_pb2.QueryOp.NOT_TEXT_MATCHES,
534 ast_pb2.QueryOp.IS_NOT_DEFINED):
535 return Fmt('{alias_col} IS NULL'), []
536
537 logging.error('unknown op: %r', op)
538
539
540 class Error(Exception):
541 """Base class for errors from this module."""
542
543
544 class NoPossibleResults(Error):
545 """The query could never match any rows from the database, so don't try.."""
OLDNEW
« no previous file with comments | « appengine/monorail/search/ast2ast.py ('k') | appengine/monorail/search/ast2sort.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698