mysqlx-connector-python 8.3.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mysqlx/expr.py ADDED
@@ -0,0 +1,1366 @@
1
+ # Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved.
2
+ #
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License, version 2.0, as
5
+ # published by the Free Software Foundation.
6
+ #
7
+ # This program is also distributed with certain software (including
8
+ # but not limited to OpenSSL) that is licensed under separate terms,
9
+ # as designated in a particular file or component or in included license
10
+ # documentation. The authors of MySQL hereby grant you an
11
+ # additional permission to link the program and your derivative works
12
+ # with the separately licensed software that they have included with
13
+ # MySQL.
14
+ #
15
+ # Without limiting anything contained in the foregoing, this file,
16
+ # which is part of MySQL Connector/Python, is also subject to the
17
+ # Universal FOSS Exception, version 1.0, a copy of which can be found at
18
+ # http://oss.oracle.com/licenses/universal-foss-exception.
19
+ #
20
+ # This program is distributed in the hope that it will be useful, but
21
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
23
+ # See the GNU General Public License, version 2.0, for more details.
24
+ #
25
+ # You should have received a copy of the GNU General Public License
26
+ # along with this program; if not, write to the Free Software Foundation, Inc.,
27
+ # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
28
+
29
+ """Expression Parser."""
30
+
31
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Union
32
+
33
+ from .dbdoc import DbDoc
34
+ from .helpers import BYTE_TYPES, get_item_or_attr
35
+ from .protobuf import Message, mysqlxpb_enum
36
+ from .types import (
37
+ BuildExprTypes,
38
+ BuildScalarTypes,
39
+ MessageType,
40
+ ProtobufMessageCextType,
41
+ ProtobufMessageType,
42
+ StrOrBytes,
43
+ )
44
+
45
+
46
+ # pylint: disable=missing-function-docstring
47
+ class TokenType:
48
+ """Token types class."""
49
+
50
+ NOT = 1
51
+ AND = 2
52
+ OR = 3
53
+ XOR = 4
54
+ IS = 5
55
+ LPAREN = 6
56
+ RPAREN = 7
57
+ LSQBRACKET = 8
58
+ RSQBRACKET = 9
59
+ BETWEEN = 10
60
+ TRUE = 11
61
+ NULL = 12
62
+ FALSE = 13
63
+ IN = 14
64
+ LIKE = 15
65
+ INTERVAL = 16
66
+ REGEXP = 17
67
+ ESCAPE = 18
68
+ IDENT = 19
69
+ LSTRING = 20
70
+ LNUM = 21
71
+ DOT = 22
72
+ DOLLAR = 23
73
+ COMMA = 24
74
+ EQ = 25
75
+ NE = 26
76
+ GT = 27
77
+ GE = 28
78
+ LT = 29
79
+ LE = 30
80
+ BITAND = 31
81
+ BITOR = 32
82
+ BITXOR = 33
83
+ LSHIFT = 34
84
+ RSHIFT = 35
85
+ PLUS = 36
86
+ MINUS = 37
87
+ MUL = 38
88
+ DIV = 39
89
+ HEX = 40
90
+ BIN = 41
91
+ NEG = 42
92
+ BANG = 43
93
+ MICROSECOND = 44
94
+ SECOND = 45
95
+ MINUTE = 46
96
+ HOUR = 47
97
+ DAY = 48
98
+ WEEK = 49
99
+ MONTH = 50
100
+ QUARTER = 51
101
+ YEAR = 52
102
+ EROTEME = 53
103
+ DOUBLESTAR = 54
104
+ MOD = 55
105
+ COLON = 56
106
+ OROR = 57
107
+ ANDAND = 58
108
+ LCURLY = 59
109
+ RCURLY = 60
110
+ CAST = 61
111
+ DOTSTAR = 62
112
+ ORDERBY_ASC = 63
113
+ ORDERBY_DESC = 64
114
+ AS = 65
115
+ ARROW = 66
116
+ QUOTE = 67
117
+ BINARY = 68
118
+ DATETIME = 69
119
+ TIME = 70
120
+ CHAR = 71
121
+ DATE = 72
122
+ DECIMAL = 73
123
+ SIGNED = 74
124
+ INTEGER = 75
125
+ UNSIGNED = 76
126
+ JSON = 77
127
+ SECOND_MICROSECOND = 78
128
+ MINUTE_MICROSECOND = 79
129
+ MINUTE_SECOND = 80
130
+ HOUR_MICROSECOND = 81
131
+ HOUR_SECOND = 82
132
+ HOUR_MINUTE = 83
133
+ DAY_MICROSECOND = 84
134
+ DAY_SECOND = 85
135
+ DAY_MINUTE = 86
136
+ DAY_HOUR = 87
137
+ YEAR_MONTH = 88
138
+ OVERLAPS = 89
139
+
140
+
141
+ _INTERVAL_UNITS = set(
142
+ [
143
+ TokenType.MICROSECOND,
144
+ TokenType.SECOND,
145
+ TokenType.MINUTE,
146
+ TokenType.HOUR,
147
+ TokenType.DAY,
148
+ TokenType.WEEK,
149
+ TokenType.MONTH,
150
+ TokenType.QUARTER,
151
+ TokenType.YEAR,
152
+ TokenType.SECOND_MICROSECOND,
153
+ TokenType.MINUTE_MICROSECOND,
154
+ TokenType.MINUTE_SECOND,
155
+ TokenType.HOUR_MICROSECOND,
156
+ TokenType.HOUR_SECOND,
157
+ TokenType.HOUR_MINUTE,
158
+ TokenType.DAY_MICROSECOND,
159
+ TokenType.DAY_SECOND,
160
+ TokenType.DAY_MINUTE,
161
+ TokenType.DAY_HOUR,
162
+ TokenType.YEAR_MONTH,
163
+ ]
164
+ )
165
+
166
+ # map of reserved word to token type
167
+ _RESERVED_WORDS = {
168
+ "and": TokenType.AND,
169
+ "or": TokenType.OR,
170
+ "xor": TokenType.XOR,
171
+ "is": TokenType.IS,
172
+ "not": TokenType.NOT,
173
+ "like": TokenType.LIKE,
174
+ "in": TokenType.IN,
175
+ "overlaps": TokenType.OVERLAPS,
176
+ "regexp": TokenType.REGEXP,
177
+ "between": TokenType.BETWEEN,
178
+ "interval": TokenType.INTERVAL,
179
+ "escape": TokenType.ESCAPE,
180
+ "cast": TokenType.CAST,
181
+ "div": TokenType.DIV,
182
+ "hex": TokenType.HEX,
183
+ "bin": TokenType.BIN,
184
+ "true": TokenType.TRUE,
185
+ "false": TokenType.FALSE,
186
+ "null": TokenType.NULL,
187
+ "second": TokenType.SECOND,
188
+ "minute": TokenType.MINUTE,
189
+ "hour": TokenType.HOUR,
190
+ "day": TokenType.DAY,
191
+ "week": TokenType.WEEK,
192
+ "month": TokenType.MONTH,
193
+ "quarter": TokenType.QUARTER,
194
+ "year": TokenType.YEAR,
195
+ "microsecond": TokenType.MICROSECOND,
196
+ "asc": TokenType.ORDERBY_ASC,
197
+ "desc": TokenType.ORDERBY_DESC,
198
+ "as": TokenType.AS,
199
+ "binary": TokenType.BINARY,
200
+ "datetime": TokenType.DATETIME,
201
+ "time": TokenType.TIME,
202
+ "char": TokenType.CHAR,
203
+ "date": TokenType.DATE,
204
+ "decimal": TokenType.DECIMAL,
205
+ "signed": TokenType.SIGNED,
206
+ "unsigned": TokenType.UNSIGNED,
207
+ "integer": TokenType.INTEGER,
208
+ "json": TokenType.JSON,
209
+ "second_microsecond": TokenType.SECOND_MICROSECOND,
210
+ "minute_microsecond": TokenType.MINUTE_MICROSECOND,
211
+ "minute_second": TokenType.MINUTE_SECOND,
212
+ "hour_microsecond": TokenType.HOUR_MICROSECOND,
213
+ "hour_second": TokenType.HOUR_SECOND,
214
+ "hour_minute": TokenType.HOUR_MINUTE,
215
+ "day_microsecond": TokenType.DAY_MICROSECOND,
216
+ "day_second": TokenType.DAY_SECOND,
217
+ "day_minute": TokenType.DAY_MINUTE,
218
+ "day_hour": TokenType.DAY_HOUR,
219
+ "year_month": TokenType.YEAR_MONTH,
220
+ }
221
+
222
+ _SQL_FUNTION_RESERVED_WORDS_COLLISION = {
223
+ "binary": TokenType.BINARY,
224
+ "cast": TokenType.CAST,
225
+ "char": TokenType.CHAR,
226
+ "date": TokenType.DATE,
227
+ "decimal": TokenType.DECIMAL,
228
+ "signed": TokenType.SIGNED,
229
+ "time": TokenType.TIME,
230
+ "unsigned": TokenType.UNSIGNED,
231
+ }
232
+
233
+ _OPERATORS = {
234
+ "=": "==",
235
+ "==": "==",
236
+ "and": "&&",
237
+ "div": "div",
238
+ "||": "||",
239
+ "or": "||",
240
+ "not": "not",
241
+ "xor": "xor",
242
+ "^": "^",
243
+ "is": "is",
244
+ "between": "between",
245
+ "in": "in",
246
+ "like": "like",
247
+ "!=": "!=",
248
+ "<>": "!=",
249
+ ">": ">",
250
+ ">=": ">=",
251
+ "<": "<",
252
+ "<=": "<=",
253
+ "&": "&",
254
+ "&&": "&&",
255
+ "|": "|",
256
+ "<<": "<<",
257
+ ">>": ">>",
258
+ "+": "+",
259
+ "-": "-",
260
+ "*": "*",
261
+ "/": "/",
262
+ "~": "~",
263
+ "%": "%",
264
+ "cast": "cast",
265
+ "cont_in": "cont_in",
266
+ "overlaps": "overlaps",
267
+ }
268
+
269
+ _UNARY_OPERATORS = {
270
+ "+": "sign_plus",
271
+ "-": "sign_minus",
272
+ "~": "~",
273
+ "not": "not",
274
+ "!": "!",
275
+ }
276
+
277
+ _NEGATION = {
278
+ "is": "is_not",
279
+ "between": "not_between",
280
+ "regexp": "not_regexp",
281
+ "like": "not_like",
282
+ "in": "not_in",
283
+ "cont_in": "not_cont_in",
284
+ "overlaps": "not_overlaps",
285
+ }
286
+
287
+
288
+ class Token:
289
+ """Token representation class."""
290
+
291
+ def __init__(self, token_type: int, value: str, length: int = 1) -> None:
292
+ self.token_type: int = token_type
293
+ self.value: str = value
294
+ self.length: int = length
295
+
296
+ def __repr__(self) -> str:
297
+ return self.__str__()
298
+
299
+ def __str__(self) -> str:
300
+ if self.token_type in (
301
+ TokenType.IDENT,
302
+ TokenType.LNUM,
303
+ TokenType.LSTRING,
304
+ ):
305
+ return f"{self.token_type}({self.value})"
306
+ return f"{self.token_type}"
307
+
308
+
309
+ # static protobuf helper functions
310
+
311
+
312
+ def build_expr(value: BuildExprTypes) -> MessageType:
313
+ msg = Message("Mysqlx.Expr.Expr")
314
+ if isinstance(value, (Message)):
315
+ return value
316
+ if isinstance(value, (ExprParser)):
317
+ return value.expr(reparse=True)
318
+ if isinstance(value, (dict, DbDoc)):
319
+ msg["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.OBJECT")
320
+ msg["object"] = build_object(value).get_message()
321
+ elif isinstance(value, (list, tuple)):
322
+ msg["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.ARRAY")
323
+ msg["array"] = build_array(value).get_message()
324
+ else:
325
+ msg["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.LITERAL")
326
+ msg["literal"] = build_scalar(value).get_message()
327
+ return msg
328
+
329
+
330
+ def build_scalar(value: BuildScalarTypes) -> MessageType:
331
+ if isinstance(value, str):
332
+ return build_string_scalar(value)
333
+ if isinstance(value, BYTE_TYPES):
334
+ return build_bytes_scalar(value)
335
+ if isinstance(value, bool):
336
+ return build_bool_scalar(value)
337
+ if isinstance(value, int):
338
+ return build_int_scalar(value)
339
+ if isinstance(value, float):
340
+ return build_double_scalar(value)
341
+ if value is None:
342
+ return build_null_scalar()
343
+ raise ValueError(f"Unsupported data type: {type(value)}")
344
+
345
+
346
+ def build_object(obj: Union[Dict, DbDoc]) -> MessageType:
347
+ if isinstance(obj, DbDoc):
348
+ return build_object(obj.__dict__)
349
+
350
+ msg = Message("Mysqlx.Expr.Object")
351
+ for key, value in obj.items():
352
+ pair = Message("Mysqlx.Expr.Object.ObjectField")
353
+ pair["key"] = key.encode() if isinstance(key, str) else key
354
+ pair["value"] = build_expr(value).get_message()
355
+ msg["fld"].extend([pair.get_message()])
356
+ return msg
357
+
358
+
359
+ def build_array(array: Sequence[BuildExprTypes]) -> MessageType:
360
+ msg = Message("Mysqlx.Expr.Array")
361
+ msg["value"].extend([build_expr(value).get_message() for value in array])
362
+ return msg
363
+
364
+
365
+ def build_null_scalar() -> MessageType:
366
+ msg = Message("Mysqlx.Datatypes.Scalar")
367
+ msg["type"] = mysqlxpb_enum("Mysqlx.Datatypes.Scalar.Type.V_NULL")
368
+ return msg
369
+
370
+
371
+ def build_double_scalar(value: float) -> MessageType:
372
+ msg = Message("Mysqlx.Datatypes.Scalar")
373
+ msg["type"] = mysqlxpb_enum("Mysqlx.Datatypes.Scalar.Type.V_DOUBLE")
374
+ msg["v_double"] = value
375
+ return msg
376
+
377
+
378
+ def build_int_scalar(value: int) -> MessageType:
379
+ msg = Message("Mysqlx.Datatypes.Scalar")
380
+ msg["type"] = mysqlxpb_enum("Mysqlx.Datatypes.Scalar.Type.V_SINT")
381
+ msg["v_signed_int"] = value
382
+ return msg
383
+
384
+
385
+ def build_unsigned_int_scalar(value: int) -> MessageType:
386
+ msg = Message("Mysqlx.Datatypes.Scalar")
387
+ msg["type"] = mysqlxpb_enum("Mysqlx.Datatypes.Scalar.Type.V_UINT")
388
+ msg["v_unsigned_int"] = value
389
+ return msg
390
+
391
+
392
+ def build_string_scalar(value: StrOrBytes) -> MessageType:
393
+ if isinstance(value, str):
394
+ value = bytes(bytearray(value, "utf-8"))
395
+ msg = Message("Mysqlx.Datatypes.Scalar")
396
+ msg["type"] = mysqlxpb_enum("Mysqlx.Datatypes.Scalar.Type.V_STRING")
397
+ msg["v_string"] = Message("Mysqlx.Datatypes.Scalar.String", value=value)
398
+ return msg
399
+
400
+
401
+ def build_bool_scalar(value: bool) -> MessageType:
402
+ msg = Message("Mysqlx.Datatypes.Scalar")
403
+ msg["type"] = mysqlxpb_enum("Mysqlx.Datatypes.Scalar.Type.V_BOOL")
404
+ msg["v_bool"] = value
405
+ return msg
406
+
407
+
408
+ def build_bytes_scalar(value: bytes) -> MessageType:
409
+ msg = Message("Mysqlx.Datatypes.Scalar")
410
+ msg["type"] = mysqlxpb_enum("Mysqlx.Datatypes.Scalar.Type.V_OCTETS")
411
+ msg["v_octets"] = Message("Mysqlx.Datatypes.Scalar.Octets", value=value)
412
+ return msg
413
+
414
+
415
+ def build_literal_expr(value: MessageType) -> MessageType:
416
+ msg = Message("Mysqlx.Expr.Expr")
417
+ msg["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.LITERAL")
418
+ msg["literal"] = value
419
+ return msg
420
+
421
+
422
+ def build_unary_op(name: str, param: MessageType) -> MessageType:
423
+ operator = Message("Mysqlx.Expr.Operator")
424
+ operator["name"] = _UNARY_OPERATORS[name]
425
+ operator["param"] = [param.get_message()]
426
+ msg = Message("Mysqlx.Expr.Expr")
427
+ msg["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.OPERATOR")
428
+ msg["operator"] = operator.get_message()
429
+ return msg
430
+
431
+
432
+ def escape_literal(string: str) -> str:
433
+ return string.replace('"', '""')
434
+
435
+
436
+ class ExprParser:
437
+ """Expression parser class."""
438
+
439
+ def __init__(self, string: str, allow_relational: bool = True) -> None:
440
+ self.string: str = string
441
+ self.tokens: List[Token] = []
442
+ self.path_name_queue: List[str] = []
443
+ self.pos: int = 0
444
+ self._allow_relational_columns: bool = allow_relational
445
+ self.placeholder_name_to_position: Dict[str, int] = {}
446
+ self.positional_placeholder_count: int = 0
447
+ self.clean_expression()
448
+ self.lex()
449
+
450
+ def __str__(self) -> str:
451
+ return f"<mysqlx.ExprParser '{self.string}'>"
452
+
453
+ def clean_expression(self) -> None:
454
+ """Removes the keywords that does not form part of the expression.
455
+
456
+ Removes the keywords "SELECT" and "WHERE" that does not form part of
457
+ the expression itself.
458
+ """
459
+ if not isinstance(self.string, str):
460
+ self.string = repr(self.string)
461
+ self.string = self.string.strip(" ")
462
+ if len(self.string) > 1 and self.string[-1] == ";":
463
+ self.string = self.string[:-1]
464
+ if "SELECT" in self.string[:6].upper():
465
+ self.string = self.string[6:]
466
+ if "WHERE" in self.string[:5].upper():
467
+ self.string = self.string[5:]
468
+
469
+ # convencience checker for lexer
470
+ def next_char_is(self, key: int, char: str) -> bool:
471
+ return key + 1 < len(self.string) and self.string[key + 1] == char
472
+
473
+ def lex_number(self, pos: int) -> Token:
474
+ # numeric literal
475
+ start = pos
476
+ found_dot = False
477
+ while pos < len(self.string) and (
478
+ self.string[pos].isdigit() or self.string[pos] == "."
479
+ ):
480
+ if self.string[pos] == ".":
481
+ if found_dot is True:
482
+ raise ValueError("Invalid number. Found multiple '.'")
483
+ found_dot = True
484
+ # technically we allow more than one "." and let float()'s parsing
485
+ # complain later
486
+ pos += 1
487
+ val = self.string[start:pos]
488
+ return Token(TokenType.LNUM, val, len(val))
489
+
490
+ def lex_alpha(self, i: int, allow_space: bool = False) -> Token:
491
+ start = i
492
+ while i < len(self.string) and (
493
+ self.string[i].isalnum()
494
+ or self.string[i] == "_"
495
+ or (self.string[i].isspace() and allow_space)
496
+ ):
497
+ i += 1
498
+
499
+ val = self.string[start:i]
500
+ try:
501
+ if (
502
+ i < len(self.string)
503
+ and self.string[i] == "("
504
+ and val.lower() not in _SQL_FUNTION_RESERVED_WORDS_COLLISION
505
+ ):
506
+ token = Token(TokenType.IDENT, val, len(val))
507
+ else:
508
+ token = Token(_RESERVED_WORDS[val.lower()], val.lower(), len(val))
509
+ except KeyError:
510
+ token = Token(TokenType.IDENT, val, len(val))
511
+ return token
512
+
513
+ def lex_quoted_token(self, key: int) -> Token:
514
+ quote_char = self.string[key]
515
+ val = ""
516
+ key += 1
517
+ start = key
518
+ while key < len(self.string):
519
+ char = self.string[key]
520
+ if (
521
+ char == quote_char
522
+ and key + 1 < len(self.string)
523
+ and self.string[key + 1] != quote_char
524
+ ):
525
+ # break if we have a quote char that's not double
526
+ break
527
+ if char in (quote_char, "\\"):
528
+ # this quote char has to be doubled
529
+ if key + 1 >= len(self.string):
530
+ break
531
+ key += 1
532
+ val += self.string[key]
533
+ else:
534
+ val += char
535
+ key += 1
536
+ if key >= len(self.string) or self.string[key] != quote_char:
537
+ raise ValueError(f"Unterminated quoted string starting at {start}")
538
+ if quote_char == "`":
539
+ return Token(TokenType.IDENT, val, len(val) + 2)
540
+ return Token(TokenType.LSTRING, val, len(val) + 2)
541
+
542
+ def lex(self) -> None:
543
+ i = 0
544
+ arrow_last = False
545
+ inside_arrow = False
546
+ while i < len(self.string):
547
+ char = self.string[i]
548
+ if char.isspace():
549
+ i += 1
550
+ continue
551
+ if char.isdigit():
552
+ token = self.lex_number(i)
553
+ elif char.isalpha() or char == "_":
554
+ token = self.lex_alpha(i, inside_arrow)
555
+ elif char == "?":
556
+ token = Token(TokenType.EROTEME, char)
557
+ elif char == ":":
558
+ token = Token(TokenType.COLON, char)
559
+ elif char == "{":
560
+ token = Token(TokenType.LCURLY, char)
561
+ elif char == "}":
562
+ token = Token(TokenType.RCURLY, char)
563
+ elif char == "+":
564
+ token = Token(TokenType.PLUS, char)
565
+ elif char == "-":
566
+ if self.next_char_is(i, ">") and not arrow_last:
567
+ token = Token(TokenType.ARROW, "->", 2)
568
+ arrow_last = True
569
+ else:
570
+ token = Token(TokenType.MINUS, char)
571
+ elif char == "*":
572
+ if self.next_char_is(i, "*"):
573
+ token = Token(TokenType.DOUBLESTAR, "**", 2)
574
+ else:
575
+ token = Token(TokenType.MUL, char)
576
+ elif char == "/":
577
+ token = Token(TokenType.DIV, char)
578
+ elif char == "$":
579
+ token = Token(TokenType.DOLLAR, char)
580
+ elif char == "%":
581
+ token = Token(TokenType.MOD, char)
582
+ elif char == "=":
583
+ if self.next_char_is(i, "="):
584
+ token = Token(TokenType.EQ, "==", 2)
585
+ else:
586
+ token = Token(TokenType.EQ, "==", 1)
587
+ elif char == "&":
588
+ if self.next_char_is(i, "&"):
589
+ token = Token(TokenType.ANDAND, "&&", 2)
590
+ else:
591
+ token = Token(TokenType.BITAND, char)
592
+ elif char == "^":
593
+ token = Token(TokenType.BITXOR, char)
594
+ elif char == "|":
595
+ if self.next_char_is(i, "|"):
596
+ token = Token(TokenType.OROR, "||", 2)
597
+ else:
598
+ token = Token(TokenType.BITOR, char)
599
+ elif char == "(":
600
+ token = Token(TokenType.LPAREN, char)
601
+ elif char == ")":
602
+ token = Token(TokenType.RPAREN, char)
603
+ elif char == "[":
604
+ token = Token(TokenType.LSQBRACKET, char)
605
+ elif char == "]":
606
+ token = Token(TokenType.RSQBRACKET, char)
607
+ elif char == "~":
608
+ token = Token(TokenType.NEG, char)
609
+ elif char == ",":
610
+ token = Token(TokenType.COMMA, char)
611
+ elif char == "!":
612
+ if self.next_char_is(i, "="):
613
+ token = Token(TokenType.NE, "!=", 2)
614
+ else:
615
+ token = Token(TokenType.BANG, char)
616
+ elif char == "<":
617
+ if self.next_char_is(i, ">"):
618
+ token = Token(TokenType.NE, "<>", 2)
619
+ elif self.next_char_is(i, "<"):
620
+ token = Token(TokenType.LSHIFT, "<<", 2)
621
+ elif self.next_char_is(i, "="):
622
+ token = Token(TokenType.LE, "<=", 2)
623
+ else:
624
+ token = Token(TokenType.LT, char)
625
+ elif char == ">":
626
+ if self.next_char_is(i, ">"):
627
+ token = Token(TokenType.RSHIFT, ">>", 2)
628
+ elif self.next_char_is(i, "="):
629
+ token = Token(TokenType.GE, ">=", 2)
630
+ else:
631
+ token = Token(TokenType.GT, char)
632
+ elif char == ".":
633
+ if self.next_char_is(i, "*"):
634
+ token = Token(TokenType.DOTSTAR, ".*", 2)
635
+ elif i + 1 < len(self.string) and self.string[i + 1].isdigit():
636
+ token = self.lex_number(i)
637
+ else:
638
+ token = Token(TokenType.DOT, char)
639
+ elif char in ("'", '"') and arrow_last:
640
+ token = Token(TokenType.QUOTE, char)
641
+ if not inside_arrow:
642
+ inside_arrow = True
643
+ else:
644
+ arrow_last = False
645
+ inside_arrow = False
646
+ elif char in ('"', "'", "`"):
647
+ token = self.lex_quoted_token(i)
648
+ else:
649
+ raise ValueError(f"Unknown character at {i}")
650
+ self.tokens.append(token)
651
+ i += token.length
652
+
653
+ def assert_cur_token(self, token_type: int) -> None:
654
+ if self.pos >= len(self.tokens):
655
+ raise ValueError(
656
+ f"Expected token type {token_type} at pos {self.pos} but no "
657
+ "tokens left"
658
+ )
659
+ if self.tokens[self.pos].token_type != token_type:
660
+ raise ValueError(
661
+ f"Expected token type {token_type} at pos {self.pos} but found "
662
+ f"type {self.tokens[self.pos]}, on tokens {self.tokens}"
663
+ )
664
+
665
+ def cur_token_type_is(self, token_type: int) -> bool:
666
+ return self.pos_token_type_is(self.pos, token_type)
667
+
668
+ def cur_token_type_in(self, *types: int) -> bool:
669
+ return self.pos < len(self.tokens) and self.tokens[self.pos].token_type in types
670
+
671
+ def next_token_type_is(self, token_type: int) -> bool:
672
+ return self.pos_token_type_is(self.pos + 1, token_type)
673
+
674
+ def next_token_type_in(self, *types: int) -> bool:
675
+ return (
676
+ self.pos < len(self.tokens)
677
+ and self.tokens[self.pos + 1].token_type in types
678
+ )
679
+
680
+ def pos_token_type_is(self, pos: int, token_type: int) -> bool:
681
+ return pos < len(self.tokens) and self.tokens[pos].token_type == token_type
682
+
683
+ def consume_token(self, token_type: int) -> str:
684
+ self.assert_cur_token(token_type)
685
+ value = self.tokens[self.pos].value
686
+ self.pos += 1
687
+ return value
688
+
689
+ def paren_expr_list(
690
+ self,
691
+ ) -> List[Union[ProtobufMessageType, ProtobufMessageCextType]]:
692
+ """Parse a paren-bounded expression list for function arguments or IN
693
+ list and return a list of Expr objects.
694
+ """
695
+ exprs = []
696
+ path_name_added = False
697
+ self.consume_token(TokenType.LPAREN)
698
+ if not self.cur_token_type_is(TokenType.RPAREN):
699
+ msg_expr = self._expr().get_message()
700
+ if hasattr(msg_expr, "identifier") and msg_expr.identifier.name:
701
+ self.path_name_queue.insert(0, msg_expr.identifier.name)
702
+ path_name_added = True
703
+ elif (
704
+ not hasattr(msg_expr, "identifier")
705
+ and "identifier" in msg_expr
706
+ and "name" in msg_expr["identifier"]
707
+ ):
708
+ self.path_name_queue.insert(0, msg_expr["identifier"]["name"])
709
+ path_name_added = True
710
+ exprs.append(msg_expr)
711
+ while self.cur_token_type_is(TokenType.COMMA):
712
+ self.pos += 1
713
+ exprs.append(self._expr().get_message())
714
+ self.consume_token(TokenType.RPAREN)
715
+ if path_name_added:
716
+ self.path_name_queue.pop()
717
+ return exprs
718
+
719
+ def identifier(self) -> MessageType:
720
+ self.assert_cur_token(TokenType.IDENT)
721
+ ident = Message("Mysqlx.Expr.Identifier")
722
+ if self.next_token_type_is(TokenType.DOT):
723
+ ident["schema_name"] = self.consume_token(TokenType.IDENT)
724
+ self.consume_token(TokenType.DOT)
725
+ ident["name"] = self.tokens[self.pos].value
726
+ self.pos += 1
727
+ return ident
728
+
729
+ def function_call(self) -> MessageType:
730
+ function_call = Message("Mysqlx.Expr.FunctionCall")
731
+ function_call["name"] = self.identifier()
732
+ function_call["param"] = self.paren_expr_list()
733
+ msg_expr = Message("Mysqlx.Expr.Expr")
734
+ msg_expr["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.FUNC_CALL")
735
+ msg_expr["function_call"] = function_call.get_message()
736
+ return msg_expr
737
+
738
+ def docpath_member(self) -> MessageType:
739
+ self.consume_token(TokenType.DOT)
740
+ token = self.tokens[self.pos]
741
+
742
+ if token.token_type == TokenType.IDENT:
743
+ if token.value.startswith("`") and token.value.endswith("`"):
744
+ raise ValueError(
745
+ f"{token.value} is not a valid JSON/ECMAScript identifier"
746
+ )
747
+ self.consume_token(TokenType.IDENT)
748
+ member_name = token.value
749
+ elif token.token_type == TokenType.LSTRING:
750
+ self.consume_token(TokenType.LSTRING)
751
+ member_name = token.value
752
+ else:
753
+ raise ValueError(
754
+ "Expected token type IDENT or LSTRING in JSON path at token "
755
+ f"pos {self.pos}"
756
+ )
757
+ doc_path_item = Message("Mysqlx.Expr.DocumentPathItem")
758
+ doc_path_item["type"] = mysqlxpb_enum(
759
+ "Mysqlx.Expr.DocumentPathItem.Type.MEMBER"
760
+ )
761
+ doc_path_item["value"] = member_name
762
+ return doc_path_item
763
+
764
+ def docpath_array_loc(self) -> MessageType:
765
+ self.consume_token(TokenType.LSQBRACKET)
766
+ if self.cur_token_type_is(TokenType.MUL):
767
+ self.consume_token(TokenType.MUL)
768
+ self.consume_token(TokenType.RSQBRACKET)
769
+ doc_path_item = Message("Mysqlx.Expr.DocumentPathItem")
770
+ doc_path_item["type"] = mysqlxpb_enum(
771
+ "Mysqlx.Expr.DocumentPathItem.Type.ARRAY_INDEX_ASTERISK"
772
+ )
773
+ return doc_path_item
774
+ if self.cur_token_type_is(TokenType.LNUM):
775
+ value = int(self.consume_token(TokenType.LNUM))
776
+ if value < 0:
777
+ raise IndexError(f"Array index cannot be negative at {self.pos}")
778
+ self.consume_token(TokenType.RSQBRACKET)
779
+ doc_path_item = Message("Mysqlx.Expr.DocumentPathItem")
780
+ doc_path_item["type"] = mysqlxpb_enum(
781
+ "Mysqlx.Expr.DocumentPathItem.Type.ARRAY_INDEX"
782
+ )
783
+ doc_path_item["index"] = value
784
+ return doc_path_item
785
+ raise ValueError(
786
+ "Exception token type MUL or LNUM in JSON path array index at "
787
+ f"token pos {self.pos}"
788
+ )
789
+
790
+ def document_field(self) -> MessageType:
791
+ if not self.tokens:
792
+ raise ValueError("Empty string cannot be used as document field")
793
+ if self.cur_token_type_is(TokenType.DOLLAR):
794
+ self.consume_token(TokenType.DOLLAR)
795
+ col_id = Message("Mysqlx.Expr.ColumnIdentifier")
796
+ if self.cur_token_type_is(TokenType.IDENT):
797
+ doc_path_item = Message("Mysqlx.Expr.DocumentPathItem")
798
+ doc_path_item["type"] = mysqlxpb_enum(
799
+ "Mysqlx.Expr.DocumentPathItem.Type.MEMBER"
800
+ )
801
+ doc_path_item["value"] = self.consume_token(TokenType.IDENT)
802
+ col_id["document_path"].extend([doc_path_item.get_message()])
803
+ col_id["document_path"].extend(self.document_path())
804
+ if self.path_name_queue:
805
+ col_id["name"] = self.path_name_queue[0]
806
+ expr = Message("Mysqlx.Expr.Expr")
807
+ expr["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.IDENT")
808
+ expr["identifier"] = col_id
809
+ return expr
810
+
811
+ def document_path(
812
+ self,
813
+ ) -> List[Union[ProtobufMessageType, ProtobufMessageCextType]]:
814
+ """Parse a JSON-style document path, like WL#7909, but prefix by @.
815
+ instead of $. We parse this as a string because the protocol doesn't
816
+ support it. (yet)
817
+ """
818
+ doc_path = []
819
+ while True:
820
+ if self.cur_token_type_is(TokenType.DOT):
821
+ doc_path.append(self.docpath_member().get_message())
822
+ elif self.cur_token_type_is(TokenType.DOTSTAR):
823
+ self.consume_token(TokenType.DOTSTAR)
824
+ doc_path_item = Message("Mysqlx.Expr.DocumentPathItem")
825
+ doc_path_item["type"] = mysqlxpb_enum(
826
+ "Mysqlx.Expr.DocumentPathItem.Type.MEMBER_ASTERISK"
827
+ )
828
+ doc_path.append(doc_path_item.get_message())
829
+ elif self.cur_token_type_is(TokenType.LSQBRACKET):
830
+ doc_path.append(self.docpath_array_loc().get_message())
831
+ elif self.cur_token_type_is(TokenType.DOUBLESTAR):
832
+ self.consume_token(TokenType.DOUBLESTAR)
833
+ doc_path_item = Message("Mysqlx.Expr.DocumentPathItem")
834
+ doc_path_item["type"] = mysqlxpb_enum(
835
+ "Mysqlx.Expr.DocumentPathItem.Type.DOUBLE_ASTERISK"
836
+ )
837
+ doc_path.append(doc_path_item.get_message())
838
+ else:
839
+ break
840
+ items = len(doc_path)
841
+ if items > 0 and get_item_or_attr(doc_path[items - 1], "type") == mysqlxpb_enum(
842
+ "Mysqlx.Expr.DocumentPathItem.Type.DOUBLE_ASTERISK"
843
+ ):
844
+ raise ValueError(f"JSON path may not end in '**' at {self.pos}")
845
+ return doc_path
846
+
847
+ def column_identifier(self) -> MessageType:
848
+ parts = []
849
+ parts.append(self.consume_token(TokenType.IDENT))
850
+ while self.cur_token_type_is(TokenType.DOT):
851
+ self.consume_token(TokenType.DOT)
852
+ parts.append(self.consume_token(TokenType.IDENT))
853
+ if len(parts) > 3:
854
+ raise ValueError(f"Too many parts to identifier at {self.pos}")
855
+ parts.reverse()
856
+ col_id = Message("Mysqlx.Expr.ColumnIdentifier")
857
+ # clever way to apply them to the struct
858
+ for i in range(0, len(parts)):
859
+ if i == 0:
860
+ col_id["name"] = parts[0]
861
+ elif i == 1:
862
+ col_id["table_name"] = parts[1]
863
+ elif i == 2:
864
+ col_id["schema_name"] = parts[2]
865
+
866
+ is_doc = False
867
+ if self.cur_token_type_is(TokenType.DOLLAR):
868
+ is_doc = True
869
+ self.consume_token(TokenType.DOLLAR)
870
+ col_id["document_path"] = self.document_path()
871
+ elif self.cur_token_type_is(TokenType.ARROW):
872
+ is_doc = True
873
+ self.consume_token(TokenType.ARROW)
874
+ is_quoted = False
875
+ if self.cur_token_type_is(TokenType.QUOTE):
876
+ is_quoted = True
877
+ self.consume_token(TokenType.QUOTE)
878
+ self.consume_token(TokenType.DOLLAR)
879
+ col_id["document_path"] = self.document_path()
880
+ if is_quoted:
881
+ self.consume_token(TokenType.QUOTE)
882
+
883
+ if is_doc and len(col_id["document_path"]) == 0:
884
+ doc_path_item = Message("Mysqlx.Expr.DocumentPathItem")
885
+ doc_path_item["type"] = mysqlxpb_enum(
886
+ "Mysqlx.Expr.DocumentPathItem.Type.MEMBER"
887
+ )
888
+ doc_path_item["value"] = ""
889
+ col_id["document_path"].extend([doc_path_item.get_message()])
890
+
891
+ msg_expr = Message("Mysqlx.Expr.Expr")
892
+ msg_expr["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.IDENT")
893
+ msg_expr["identifier"] = col_id
894
+ return msg_expr
895
+
896
+ def next_token(self) -> Token:
897
+ if self.pos >= len(self.tokens):
898
+ raise ValueError("Unexpected end of token stream")
899
+ token = self.tokens[self.pos]
900
+ self.pos += 1
901
+ return token
902
+
903
+ def expect_token(self, token_type: int) -> None:
904
+ token = self.next_token()
905
+ if token.token_type != token_type:
906
+ raise ValueError(f"Expected token type {token_type}")
907
+
908
+ def peek_token(self) -> Token:
909
+ return self.tokens[self.pos]
910
+
911
+ def consume_any_token(self) -> str:
912
+ value = self.tokens[self.pos].value
913
+ self.pos += 1
914
+ return value
915
+
916
+ def parse_json_array(self) -> MessageType:
917
+ """
918
+ jsonArray ::= "[" [ expr ("," expr)* ] "]"
919
+ """
920
+ msg = Message("Mysqlx.Expr.Array")
921
+ while self.pos < len(self.tokens) and not self.cur_token_type_is(
922
+ TokenType.RSQBRACKET
923
+ ):
924
+ msg["value"].extend([self._expr().get_message()])
925
+ if not self.cur_token_type_is(TokenType.COMMA):
926
+ break
927
+ self.consume_token(TokenType.COMMA)
928
+ self.consume_token(TokenType.RSQBRACKET)
929
+
930
+ expr = Message("Mysqlx.Expr.Expr")
931
+ expr["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.ARRAY")
932
+ expr["array"] = msg.get_message()
933
+ return expr
934
+
935
+ def parse_json_doc(self) -> MessageType:
936
+ """
937
+ jsonDoc ::= "{" [jsonKeyValue ("," jsonKeyValue)*] "}"
938
+ jsonKeyValue ::= STRING_DQ ":" expr
939
+ """
940
+ msg = Message("Mysqlx.Expr.Object")
941
+ while self.pos < len(self.tokens) and not self.cur_token_type_is(
942
+ TokenType.RCURLY
943
+ ):
944
+ item = Message("Mysqlx.Expr.Object.ObjectField")
945
+ item["key"] = self.consume_token(TokenType.LSTRING)
946
+ self.consume_token(TokenType.COLON)
947
+ item["value"] = self._expr().get_message()
948
+ msg["fld"].extend([item.get_message()])
949
+ if not self.cur_token_type_is(TokenType.COMMA):
950
+ break
951
+ self.consume_token(TokenType.COMMA)
952
+ self.consume_token(TokenType.RCURLY)
953
+
954
+ expr = Message("Mysqlx.Expr.Expr")
955
+ expr["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.OBJECT")
956
+ expr["object"] = msg.get_message()
957
+ return expr
958
+
959
+ def parse_place_holder(self, token: Token) -> MessageType:
960
+ place_holder_name = ""
961
+ if self.cur_token_type_is(TokenType.LNUM):
962
+ place_holder_name = self.consume_token(TokenType.LNUM)
963
+ elif self.cur_token_type_is(TokenType.IDENT):
964
+ place_holder_name = self.consume_token(TokenType.IDENT)
965
+ elif token.token_type == TokenType.EROTEME:
966
+ place_holder_name = str(self.positional_placeholder_count)
967
+ else:
968
+ raise ValueError(f"Invalid placeholder name at token pos {self.pos}")
969
+
970
+ msg_expr = Message("Mysqlx.Expr.Expr")
971
+ msg_expr["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.PLACEHOLDER")
972
+ if place_holder_name in self.placeholder_name_to_position:
973
+ msg_expr["position"] = self.placeholder_name_to_position[place_holder_name]
974
+ else:
975
+ msg_expr["position"] = self.positional_placeholder_count
976
+ self.placeholder_name_to_position[
977
+ place_holder_name
978
+ ] = self.positional_placeholder_count
979
+ self.positional_placeholder_count += 1
980
+ return msg_expr
981
+
982
+ def cast(self) -> MessageType:
983
+ """cast ::= CAST LPAREN expr AS cast_data_type RPAREN"""
984
+ operator = Message("Mysqlx.Expr.Operator", name="cast")
985
+ self.consume_token(TokenType.LPAREN)
986
+ operator["param"].extend([self._expr().get_message()])
987
+ self.consume_token(TokenType.AS)
988
+
989
+ type_scalar = build_bytes_scalar(str.encode(self.cast_data_type()))
990
+ operator["param"].extend([build_literal_expr(type_scalar).get_message()])
991
+ self.consume_token(TokenType.RPAREN)
992
+ msg = Message("Mysqlx.Expr.Expr", operator=operator.get_message())
993
+ msg["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.OPERATOR")
994
+ return msg
995
+
996
+ def cast_data_type(self) -> str:
997
+ """cast_data_type ::= ( BINARY dimension? ) |
998
+ ( CHAR dimension? ) |
999
+ ( DATE ) |
1000
+ ( DATETIME dimension? ) |
1001
+ ( TIME dimension? ) |
1002
+ ( DECIMAL dimension? ) |
1003
+ ( SIGNED INTEGER? ) |
1004
+ ( UNSIGNED INTEGER? ) |
1005
+ JSON
1006
+ """
1007
+ token = self.next_token()
1008
+ if token.token_type in (
1009
+ TokenType.BINARY,
1010
+ TokenType.CHAR,
1011
+ TokenType.DATETIME,
1012
+ TokenType.TIME,
1013
+ ):
1014
+ dimension = self.cast_data_type_dimension()
1015
+ return f"{token.value}{dimension}" if dimension else token.value
1016
+ if token.token_type is TokenType.DECIMAL:
1017
+ dimension = self.cast_data_type_dimension(True)
1018
+ return f"{token.value}{dimension}" if dimension else token.value
1019
+ if token.token_type in (TokenType.SIGNED, TokenType.UNSIGNED):
1020
+ if self.cur_token_type_is(TokenType.INTEGER):
1021
+ self.consume_token(TokenType.INTEGER)
1022
+ return token.value
1023
+ if token.token_type in (
1024
+ TokenType.INTEGER,
1025
+ TokenType.JSON,
1026
+ TokenType.DATE,
1027
+ ):
1028
+ return token.value
1029
+
1030
+ raise ValueError(
1031
+ f"Unknown token type {token.token_type} at position {self.pos} "
1032
+ f"({token.value})"
1033
+ )
1034
+
1035
+ def cast_data_type_dimension(self, decimal: bool = False) -> Optional[str]:
1036
+ """dimension ::= LPAREN LNUM (, LNUM)? RPAREN"""
1037
+ if not self.cur_token_type_is(TokenType.LPAREN):
1038
+ return None
1039
+
1040
+ dimension = []
1041
+ self.consume_token(TokenType.LPAREN)
1042
+ dimension.append(self.consume_token(TokenType.LNUM))
1043
+ if decimal and self.cur_token_type_is(TokenType.COMMA):
1044
+ self.consume_token(TokenType.COMMA)
1045
+ dimension.append(self.consume_token(TokenType.LNUM))
1046
+ self.consume_token(TokenType.RPAREN)
1047
+
1048
+ return (
1049
+ f"({dimension[0]})"
1050
+ if len(dimension) == 1
1051
+ else f"({dimension[0]},{dimension[1]})"
1052
+ )
1053
+
1054
+ @staticmethod
1055
+ def star_operator() -> MessageType:
1056
+ msg = Message("Mysqlx.Expr.Expr")
1057
+ msg["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.OPERATOR")
1058
+ msg["operator"] = Message("Mysqlx.Expr.Operator", name="*")
1059
+ return msg
1060
+
1061
+ def atomic_expr(self) -> MessageType:
1062
+ """Parse an atomic expression and return a protobuf Expr object"""
1063
+ token = self.next_token()
1064
+
1065
+ if token.token_type in [TokenType.EROTEME, TokenType.COLON]:
1066
+ return self.parse_place_holder(token)
1067
+ if token.token_type == TokenType.LCURLY:
1068
+ return self.parse_json_doc()
1069
+ if token.token_type == TokenType.LSQBRACKET:
1070
+ return self.parse_json_array()
1071
+ if token.token_type == TokenType.CAST:
1072
+ return self.cast()
1073
+ if token.token_type == TokenType.LPAREN:
1074
+ expr = self._expr()
1075
+ self.expect_token(TokenType.RPAREN)
1076
+ return expr
1077
+ if token.token_type in [TokenType.PLUS, TokenType.MINUS]:
1078
+ peek = self.peek_token()
1079
+ if peek.token_type == TokenType.LNUM:
1080
+ self.tokens[self.pos].value = token.value + peek.value
1081
+ return self.atomic_expr()
1082
+ return build_unary_op(token.value, self.atomic_expr())
1083
+ if token.token_type in (TokenType.NOT, TokenType.NEG, TokenType.BANG):
1084
+ return build_unary_op(token.value, self.atomic_expr())
1085
+ if token.token_type == TokenType.LSTRING:
1086
+ return build_literal_expr(build_string_scalar(token.value))
1087
+ if token.token_type == TokenType.NULL:
1088
+ return build_literal_expr(build_null_scalar())
1089
+ if token.token_type == TokenType.LNUM:
1090
+ if "." in token.value:
1091
+ return build_literal_expr(build_double_scalar(float(token.value)))
1092
+ return build_literal_expr(build_int_scalar(int(token.value)))
1093
+ if token.token_type in [TokenType.TRUE, TokenType.FALSE]:
1094
+ return build_literal_expr(
1095
+ build_bool_scalar(token.token_type == TokenType.TRUE)
1096
+ )
1097
+ if token.token_type == TokenType.DOLLAR:
1098
+ return self.document_field()
1099
+ if token.token_type == TokenType.MUL:
1100
+ return self.star_operator()
1101
+ if token.token_type == TokenType.IDENT:
1102
+ self.pos = self.pos - 1 # stay on the identifier
1103
+ if self.next_token_type_is(TokenType.LPAREN) or (
1104
+ self.next_token_type_is(TokenType.DOT)
1105
+ and self.pos_token_type_is(self.pos + 2, TokenType.IDENT)
1106
+ and self.pos_token_type_is(self.pos + 3, TokenType.LPAREN)
1107
+ ):
1108
+ # Function call
1109
+ return self.function_call()
1110
+ return (
1111
+ self.document_field()
1112
+ if not self._allow_relational_columns
1113
+ else self.column_identifier()
1114
+ )
1115
+
1116
+ raise ValueError(
1117
+ f"Unknown token type = {token.token_type} when expecting atomic "
1118
+ f"expression at {self.pos}"
1119
+ )
1120
+
1121
+ def parse_left_assoc_binary_op_expr(
1122
+ self, types: Iterable[int], inner_parser: Callable[[], MessageType]
1123
+ ) -> MessageType:
1124
+ """Given a `set' of types and an Expr-returning inner parser function,
1125
+ parse a left associate binary operator expression"""
1126
+ lhs = inner_parser()
1127
+ while self.pos < len(self.tokens) and self.tokens[self.pos].token_type in types:
1128
+ msg = Message("Mysqlx.Expr.Expr")
1129
+ msg["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.OPERATOR")
1130
+ operator = Message("Mysqlx.Expr.Operator")
1131
+ operator["name"] = _OPERATORS[self.tokens[self.pos].value]
1132
+ operator["param"] = [lhs.get_message()]
1133
+ self.pos += 1
1134
+ operator["param"].extend([inner_parser().get_message()])
1135
+ msg["operator"] = operator
1136
+ lhs = msg
1137
+ return lhs
1138
+
1139
+ # operator precedence is implemented here
1140
+ def add_sub_interval(self) -> MessageType:
1141
+ lhs = self.atomic_expr()
1142
+ if self.cur_token_type_in(
1143
+ TokenType.PLUS, TokenType.MINUS
1144
+ ) and self.next_token_type_is(TokenType.INTERVAL):
1145
+ token = self.next_token()
1146
+
1147
+ operator = Message("Mysqlx.Expr.Operator")
1148
+ operator["param"].extend([lhs.get_message()])
1149
+ operator["name"] = (
1150
+ "date_add" if token.token_type is TokenType.PLUS else "date_sub"
1151
+ )
1152
+
1153
+ self.consume_token(TokenType.INTERVAL)
1154
+ operator["param"].extend([self.bit_expr().get_message()])
1155
+
1156
+ if not self.cur_token_type_in(*_INTERVAL_UNITS):
1157
+ raise ValueError(f"Expected interval type at position {self.pos}")
1158
+
1159
+ token = str.encode(self.consume_any_token().upper())
1160
+ operator["param"].extend(
1161
+ [build_literal_expr(build_bytes_scalar(token)).get_message()]
1162
+ )
1163
+
1164
+ lhs = Message("Mysqlx.Expr.Expr", operator=operator)
1165
+ lhs["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.OPERATOR")
1166
+
1167
+ return lhs
1168
+
1169
+ def mul_div_expr(self) -> MessageType:
1170
+ return self.parse_left_assoc_binary_op_expr(
1171
+ set([TokenType.MUL, TokenType.DIV, TokenType.MOD]),
1172
+ self.add_sub_interval,
1173
+ )
1174
+
1175
+ def add_sub_expr(self) -> MessageType:
1176
+ return self.parse_left_assoc_binary_op_expr(
1177
+ set([TokenType.PLUS, TokenType.MINUS]), self.mul_div_expr
1178
+ )
1179
+
1180
+ def shift_expr(self) -> MessageType:
1181
+ return self.parse_left_assoc_binary_op_expr(
1182
+ set([TokenType.LSHIFT, TokenType.RSHIFT]), self.add_sub_expr
1183
+ )
1184
+
1185
+ def bit_expr(self) -> MessageType:
1186
+ return self.parse_left_assoc_binary_op_expr(
1187
+ set([TokenType.BITAND, TokenType.BITOR, TokenType.BITXOR]),
1188
+ self.shift_expr,
1189
+ )
1190
+
1191
+ def comp_expr(self) -> MessageType:
1192
+ return self.parse_left_assoc_binary_op_expr(
1193
+ set(
1194
+ [
1195
+ TokenType.GE,
1196
+ TokenType.GT,
1197
+ TokenType.LE,
1198
+ TokenType.LT,
1199
+ TokenType.EQ,
1200
+ TokenType.NE,
1201
+ ]
1202
+ ),
1203
+ self.bit_expr,
1204
+ )
1205
+
1206
+ def ilri_expr(self) -> MessageType:
1207
+ params = []
1208
+ lhs = self.comp_expr()
1209
+ is_not = False
1210
+ if self.cur_token_type_is(TokenType.NOT):
1211
+ is_not = True
1212
+ self.consume_token(TokenType.NOT)
1213
+ if self.pos < len(self.tokens):
1214
+ params.append(lhs.get_message())
1215
+ op_name = self.tokens[self.pos].value
1216
+ if self.cur_token_type_is(TokenType.IS):
1217
+ self.consume_token(TokenType.IS)
1218
+ # for IS, NOT comes AFTER
1219
+ if self.cur_token_type_is(TokenType.NOT):
1220
+ is_not = True
1221
+ self.consume_token(TokenType.NOT)
1222
+ params.append(self.comp_expr().get_message())
1223
+ elif self.cur_token_type_is(TokenType.IN):
1224
+ self.consume_token(TokenType.IN)
1225
+ if self.cur_token_type_is(TokenType.LPAREN):
1226
+ params.extend(self.paren_expr_list())
1227
+ else:
1228
+ op_name = "cont_in"
1229
+ params.append(self.comp_expr().get_message())
1230
+ elif self.cur_token_type_is(TokenType.OVERLAPS):
1231
+ self.consume_token(TokenType.OVERLAPS)
1232
+ params.append(self.comp_expr().get_message())
1233
+
1234
+ elif self.cur_token_type_is(TokenType.LIKE):
1235
+ self.consume_token(TokenType.LIKE)
1236
+ params.append(self.comp_expr().get_message())
1237
+ if self.cur_token_type_is(TokenType.ESCAPE):
1238
+ self.consume_token(TokenType.ESCAPE)
1239
+ params.append(self.comp_expr().get_message())
1240
+ elif self.cur_token_type_is(TokenType.BETWEEN):
1241
+ self.consume_token(TokenType.BETWEEN)
1242
+ params.append(self.comp_expr().get_message())
1243
+ self.consume_token(TokenType.AND)
1244
+ params.append(self.comp_expr().get_message())
1245
+ elif self.cur_token_type_is(TokenType.REGEXP):
1246
+ self.consume_token(TokenType.REGEXP)
1247
+ params.append(self.comp_expr().get_message())
1248
+ else:
1249
+ if is_not:
1250
+ raise ValueError(f"Unknown token after NOT as pos {self.pos}")
1251
+ op_name = None # not an operator we're interested in
1252
+ if op_name:
1253
+ operator = Message("Mysqlx.Expr.Operator")
1254
+ operator["name"] = _NEGATION[op_name] if is_not else op_name
1255
+ operator["param"] = params
1256
+ msg_expr = Message("Mysqlx.Expr.Expr")
1257
+ msg_expr["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.OPERATOR")
1258
+ msg_expr["operator"] = operator.get_message()
1259
+ lhs = msg_expr
1260
+ return lhs
1261
+
1262
+ def and_expr(self) -> MessageType:
1263
+ return self.parse_left_assoc_binary_op_expr(
1264
+ set([TokenType.AND, TokenType.ANDAND]), self.ilri_expr
1265
+ )
1266
+
1267
+ def xor_expr(self) -> MessageType:
1268
+ return self.parse_left_assoc_binary_op_expr(set([TokenType.XOR]), self.and_expr)
1269
+
1270
+ def or_expr(self) -> MessageType:
1271
+ return self.parse_left_assoc_binary_op_expr(
1272
+ set([TokenType.OR, TokenType.OROR]), self.xor_expr
1273
+ )
1274
+
1275
+ def _expr(self, reparse: bool = False) -> MessageType:
1276
+ if reparse:
1277
+ self.tokens = []
1278
+ self.pos = 0
1279
+ self.placeholder_name_to_position = {}
1280
+ self.positional_placeholder_count = 0
1281
+ self.lex()
1282
+ return self.or_expr()
1283
+
1284
+ def expr(self, reparse: bool = False) -> MessageType:
1285
+ expression = self._expr(reparse)
1286
+ used_tokens = self.pos
1287
+ if self.pos_token_type_is(len(self.tokens) - 2, TokenType.AS):
1288
+ used_tokens += 2
1289
+ if used_tokens < len(self.tokens):
1290
+ raise ValueError(
1291
+ f"Unused token types {self.tokens[self.pos :]} found in "
1292
+ f"expression at position: {self.pos}"
1293
+ )
1294
+ return expression
1295
+
1296
+ def parse_table_insert_field(self) -> MessageType:
1297
+ return Message("Mysqlx.Crud.Column", name=self.consume_token(TokenType.IDENT))
1298
+
1299
+ def parse_table_update_field(self) -> Any:
1300
+ return self.column_identifier().identifier
1301
+
1302
+ def _table_fields(self) -> List[str]:
1303
+ fields = []
1304
+ temp = self.string.split(",")
1305
+ temp.reverse()
1306
+ while temp:
1307
+ field = temp.pop()
1308
+ while (
1309
+ field.count("(") != field.count(")")
1310
+ or field.count("[") != field.count("]")
1311
+ or field.count("{") != field.count("}")
1312
+ ):
1313
+ field = f"{temp.pop()},{field}"
1314
+ fields.append(field.strip())
1315
+ return fields
1316
+
1317
+ def parse_table_select_projection(
1318
+ self,
1319
+ ) -> List[Union[ProtobufMessageType, ProtobufMessageCextType]]:
1320
+ project_expr: List = []
1321
+ first = True
1322
+ fields = self._table_fields()
1323
+ while self.pos < len(self.tokens):
1324
+ if not first:
1325
+ self.consume_token(TokenType.COMMA)
1326
+ first = False
1327
+ projection = Message("Mysqlx.Crud.Projection", source=self._expr())
1328
+ if self.cur_token_type_is(TokenType.AS):
1329
+ self.consume_token(TokenType.AS)
1330
+ projection["alias"] = self.consume_token(TokenType.IDENT)
1331
+ else:
1332
+ projection["alias"] = fields[len(project_expr)]
1333
+ project_expr.append(projection.get_message())
1334
+
1335
+ return project_expr
1336
+
1337
+ def parse_order_spec(
1338
+ self,
1339
+ ) -> List[Union[ProtobufMessageType, ProtobufMessageCextType]]:
1340
+ order_specs = []
1341
+ first = True
1342
+ while self.pos < len(self.tokens):
1343
+ if not first:
1344
+ self.consume_token(TokenType.COMMA)
1345
+ first = False
1346
+ order = Message("Mysqlx.Crud.Order", expr=self._expr())
1347
+ if self.cur_token_type_is(TokenType.ORDERBY_ASC):
1348
+ order["direction"] = mysqlxpb_enum("Mysqlx.Crud.Order.Direction.ASC")
1349
+ self.consume_token(TokenType.ORDERBY_ASC)
1350
+ elif self.cur_token_type_is(TokenType.ORDERBY_DESC):
1351
+ order["direction"] = mysqlxpb_enum("Mysqlx.Crud.Order.Direction.DESC")
1352
+ self.consume_token(TokenType.ORDERBY_DESC)
1353
+ order_specs.append(order.get_message())
1354
+ return order_specs
1355
+
1356
+ def parse_expr_list(
1357
+ self,
1358
+ ) -> List[Union[ProtobufMessageType, ProtobufMessageCextType]]:
1359
+ expr_list = []
1360
+ first = True
1361
+ while self.pos < len(self.tokens):
1362
+ if not first:
1363
+ self.consume_token(TokenType.COMMA)
1364
+ first = False
1365
+ expr_list.append(self._expr().get_message())
1366
+ return expr_list