Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 1 | # Copyright 2016 The Chromium Authors |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 4 | |
| 5 | """Protocol buffers for user queries parsed into abstract syntax trees. |
| 6 | |
| 7 | A user issue query can look like [Type=Defect owner:jrobbins "memory leak"]. |
| 8 | In that simple form, all the individual search conditions are simply ANDed |
| 9 | together. In the code, a list of conditions to be ANDed is called a |
| 10 | conjunction. |
| 11 | |
| 12 | Monorail also supports a quick-or feature: [Type=Defect,Enhancement]. That |
| 13 | will match any issue that has labels Type-Defect or Type-Enhancement, or both. |
| 14 | |
| 15 | Monorail supports a top-level "OR" keyword that can |
| 16 | be used to logically OR a series of conjunctions. For example: |
| 17 | [Type=Defect stars>10 OR Type=Enhancement stars>50]. |
| 18 | |
| 19 | Parentheses groups and "OR" statements are preprocessed before the final |
| 20 | QueryAST is constructed. |
| 21 | |
| 22 | So, QueryAST is always exactly two levels: the overall tree |
| 23 | consists of a list of conjunctions, and each conjunction consists of a list |
| 24 | of conditions. |
| 25 | |
| 26 | A condition can look like [stars>10] or [summary:memory] or |
| 27 | [Type=Defect,Enhancement]. Each condition has a single comparison operator. |
| 28 | Most conditions refer to a single field definition, but in the case of |
| 29 | cross-project search a single condition can have a list of field definitions |
| 30 | from the different projects being searched. Each condition can have a list |
| 31 | of constant values to compare against. The values may be all strings or all |
| 32 | integers. |
| 33 | |
| 34 | Some conditions are procesed by the SQL database and others by the GAE |
| 35 | search API. All conditions are passed to each module and it is up to |
| 36 | the module to decide which conditions to handle and which to ignore. |
| 37 | """ |
| 38 | |
| 39 | from __future__ import print_function |
| 40 | from __future__ import division |
| 41 | from __future__ import absolute_import |
| 42 | |
| 43 | from protorpc import messages |
| 44 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 45 | from mrproto import tracker_pb2 |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 46 | |
| 47 | |
| 48 | # This is a special field_name for a FieldDef that means to do a fulltext |
| 49 | # search for words that occur in any part of the issue. |
| 50 | ANY_FIELD = 'any_field' |
| 51 | |
| 52 | |
| 53 | class QueryOp(messages.Enum): |
| 54 | """Enumeration of possible query condition operators.""" |
| 55 | EQ = 1 |
| 56 | NE = 2 |
| 57 | LT = 3 |
| 58 | GT = 4 |
| 59 | LE = 5 |
| 60 | GE = 6 |
| 61 | TEXT_HAS = 7 |
| 62 | NOT_TEXT_HAS = 8 |
| 63 | IS_DEFINED = 11 |
| 64 | IS_NOT_DEFINED = 12 |
| 65 | KEY_HAS = 13 |
| 66 | |
| 67 | |
| 68 | class TokenType(messages.Enum): |
| 69 | """Enumeration of query tokens used for parentheses parsing.""" |
| 70 | SUBQUERY = 1 |
| 71 | LEFT_PAREN = 2 |
| 72 | RIGHT_PAREN = 3 |
| 73 | OR = 4 |
| 74 | |
| 75 | |
| 76 | class QueryToken(messages.Message): |
| 77 | """Data structure to represent a single token for parentheses parsing.""" |
| 78 | token_type = messages.EnumField(TokenType, 1, required=True) |
| 79 | value = messages.StringField(2) |
| 80 | |
| 81 | |
| 82 | class Condition(messages.Message): |
| 83 | """Representation of one query condition. E.g., [Type=Defect,Task].""" |
| 84 | op = messages.EnumField(QueryOp, 1, required=True) |
| 85 | field_defs = messages.MessageField(tracker_pb2.FieldDef, 2, repeated=True) |
| 86 | str_values = messages.StringField(3, repeated=True) |
| 87 | int_values = messages.IntegerField(4, repeated=True) |
| 88 | # The suffix of a search field |
| 89 | # eg. the 'approver' in 'UXReview-approver:user@mail.com' |
| 90 | key_suffix = messages.StringField(5) |
| 91 | # The name of the phase this field value should belong to. |
| 92 | phase_name = messages.StringField(6) |
| 93 | |
| 94 | |
| 95 | class Conjunction(messages.Message): |
| 96 | """A list of conditions that are implicitly ANDed together.""" |
| 97 | conds = messages.MessageField(Condition, 1, repeated=True) |
| 98 | |
| 99 | |
| 100 | class QueryAST(messages.Message): |
| 101 | """Abstract syntax tree for the user's query.""" |
| 102 | conjunctions = messages.MessageField(Conjunction, 1, repeated=True) |
| 103 | |
| 104 | |
| 105 | def MakeCond(op, field_defs, str_values, int_values, |
| 106 | key_suffix=None, phase_name=None): |
| 107 | """Shorthand function to construct a Condition PB.""" |
| 108 | return Condition( |
| 109 | op=op, field_defs=field_defs, str_values=str_values, |
| 110 | int_values=int_values, key_suffix=key_suffix, phase_name=phase_name) |