D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
opt
/
hc_python
/
lib
/
python3.8
/
site-packages
/
mysqlx
/
Filename :
expr.py
back
Copy
# MySQL Connector/Python - MySQL driver written in Python. # Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most # MySQL Connectors. There are special exceptions to the terms and # conditions of the GPLv2 as it is applied to this software, see the # FOSS License Exception # <http://www.mysql.com/about/legal/licensing/foss-exception.html>. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA from .compat import STRING_TYPES, PY3 from .protobuf.mysqlx_datatypes_pb2 import Scalar from .protobuf.mysqlx_expr_pb2 import ColumnIdentifier, DocumentPathItem, Expr from .protobuf.mysqlx_crud_pb2 import Column, Order, Projection if PY3: xrange = range class TokenType: NOT = 1 AND = 2 OR = 3 XOR = 4 IS = 5 LPAREN = 6 RPAREN = 7 LSQBRACKET = 8 RSQBRACKET = 9 BETWEEN = 10 TRUE = 11 NULL = 12 FALSE = 13 IN = 14 LIKE = 15 INTERVAL = 16 REGEXP = 17 ESCAPE = 18 IDENT = 19 LSTRING = 20 LNUM = 21 DOT = 22 DOLLAR = 23 COMMA = 24 EQ = 25 NE = 26 GT = 27 GE = 28 LT = 29 LE = 30 BITAND = 31 BITOR = 32 BITXOR = 33 LSHIFT = 34 RSHIFT = 35 PLUS = 36 MINUS = 37 MUL = 38 DIV = 39 HEX = 40 BIN = 41 NEG = 42 BANG = 43 MICROSECOND = 44 SECOND = 45 MINUTE = 46 HOUR = 47 DAY = 48 WEEK = 49 MONTH = 50 QUARTER = 51 YEAR = 52 EROTEME = 53 DOUBLESTAR = 54 MOD = 55 COLON = 56 OROR = 57 ANDAND = 58 LCURLY = 59 RCURLY = 60 CAST = 61 DOTSTAR = 62 ORDERBY_ASC = 63 ORDERBY_DESC = 64 AS = 65 interval_units = set([ TokenType.MICROSECOND, TokenType.SECOND, TokenType.MINUTE, TokenType.HOUR, TokenType.DAY, TokenType.WEEK, TokenType.MONTH, TokenType.QUARTER, TokenType.YEAR]) # map of reserved word to token type reservedWords = { "and": TokenType.AND, "or": TokenType.OR, "xor": TokenType.XOR, "is": TokenType.IS, "not": TokenType.NOT, "like": TokenType.LIKE, "in": TokenType.IN, "regexp": TokenType.REGEXP, "between": TokenType.BETWEEN, "interval": TokenType.INTERVAL, "escape": TokenType.ESCAPE, "cast": TokenType.CAST, "div": TokenType.DIV, "hex": TokenType.HEX, "bin": TokenType.BIN, "true": TokenType.TRUE, "false": TokenType.FALSE, "null": TokenType.NULL, "second": TokenType.SECOND, "minute": TokenType.MINUTE, "hour": TokenType.HOUR, "day": TokenType.DAY, "week": TokenType.WEEK, "month": TokenType.MONTH, "quarter": TokenType.QUARTER, "year": TokenType.YEAR, "microsecond": TokenType.MICROSECOND, "asc": TokenType.ORDERBY_ASC, "desc": TokenType.ORDERBY_DESC, "as": TokenType.AS } class Token: def __init__(self, type, val, len=1): self.type = type self.val = val self.len = len def __repr__(self): return self.__str__() def __str__(self): if self.type == TokenType.IDENT or self.type == TokenType.LNUM or \ self.type == TokenType.LSTRING: return str(self.type) + "(" + self.val + ")" else: return str(self.type) # static protobuf helper functions def build_null_scalar(): return Scalar(type=Scalar.V_NULL) def build_double_scalar(d): return Scalar(type=Scalar.V_DOUBLE, v_double=d) def build_int_scalar(i): return Scalar(type=Scalar.V_SINT, v_signed_int=i) def build_string_scalar(s): if isinstance(s, STRING_TYPES): s = bytes(bytearray(s, "utf-8")) return Scalar(type=Scalar.V_STRING, v_string=Scalar.String(value=s)) def build_bool_scalar(b): s = Scalar() s.type = Scalar.V_BOOL s.v_bool = b return s def build_literal_expr(anyval): e = Expr() e.type = Expr.LITERAL e.literal.CopyFrom(anyval) return e def build_unary_op(name, param): e = Expr() e.type = Expr.OPERATOR e.operator.name = name e.operator.param.add().CopyFrom(param) return e def escape_literal(string): return string.replace('"', '""') class ExprParser: def __init__(self, string, allowRelational): self.string = string self.tokens = [] self.pos = 0 self._allowRelationalColumns = allowRelational self.placeholder_name_to_position = {} self.positional_placeholder_count = 0 self.lex() # convencience checker for lexer def next_char_is(self, i, c): return i + 1 < len(self.string) and self.string[i + 1] == c def lex_number(self, pos): # numeric literal start = pos found_dot = False while pos < len(self.string) and (self.string[pos].isdigit() or self.string[pos] == "."): if self.string[pos] == ".": if found_dot is True: raise ValueError("Invalid number. Found multiple '.'") found_dot = True # technically we allow more than one "." and let float()'s parsing # complain later pos = pos + 1 val = self.string[start:pos] t = Token(TokenType.LNUM, val, len(val)) return t def lex_alpha(self, i): start = i while i < len(self.string) and (self.string[i].isalnum() or self.string[i] == "_"): i = i + 1 val = self.string[start:i] try: token = Token(reservedWords[val.lower()], val.upper(), len(val)) except KeyError: token = Token(TokenType.IDENT, val, len(val)) return token def lex_quoted_token(self, i): quote_char = self.string[i] val = "" i += 1 start = i while i < len(self.string): c = self.string[i] if c == quote_char and i + 1 < len(self.string) and \ self.string[i + 1] != quote_char: # break if we have a quote char that's not double break elif c == quote_char or c == "\\": # this quote char has to be doubled if i + 1 >= len(self.string): break i = i + 1 val = val + self.string[i] else: val = val + c i = i + 1 if i >= len(self.string) or self.string[i] != quote_char: raise ValueError("Unterminated quoted string starting at {0}" "".format(start)) if quote_char == "`": return Token(TokenType.IDENT, val, len(val)+2) else: return Token(TokenType.LSTRING, val, len(val)+2) def lex(self): i = 0 while i < len(self.string): c = self.string[i] if c.isspace(): i += 1 continue elif c.isdigit(): token = self.lex_number(i) elif c.isalpha() or c == "_": token = self.lex_alpha(i) elif c == "?": token = Token(TokenType.EROTEME, c) elif c == ":": token = Token(TokenType.COLON, c) elif c == "{": token = Token(TokenType.LCURLY, c) elif c == "}": token = Token(TokenType.RCURLY, c) elif c == "+": token = Token(TokenType.PLUS, c) elif c == "-": Token(TokenType.MINUS, c) elif c == "*": if self.next_char_is(i, "*"): token = Token(TokenType.DOUBLESTAR, "**") else: token = Token(TokenType.MUL, c) elif c == "/": token = Token(TokenType.DIV, c) elif c == "$": token = Token(TokenType.DOLLAR, c) elif c == "%": token = Token(TokenType.MOD, c) elif c == "=": if self.next_char_is(i, "="): token = Token(TokenType.EQ, "==", 2) else: token = Token(TokenType.EQ, "==", 1) elif c == "&": if self.next_char_is(i, "&"): token = Token(TokenType.ANDAND, c) else: token = Token(TokenType.BITAND, c) elif c == "|": if self.next_char_is(i, "|"): token = Token(TokenType.OROR, "||") else: token = Token(TokenType.BITOR, c) elif c == "(": token = Token(TokenType.LPAREN, c) elif c == ")": token = Token(TokenType.RPAREN, c) elif c == "[": token = Token(TokenType.LSQBRACKET, c) elif c == "]": token = Token(TokenType.RSQBRACKET, c) elif c == "~": token = Token(TokenType.NEG, c) elif c == ",": token = Token(TokenType.COMMA, c) elif c == "!": if self.next_char_is(i, "="): token = Token(TokenType.NE, "!=") else: token = Token(TokenType.BANG, c) elif c == "<": if self.next_char_is(i, "<"): token = Token(TokenType.LSHIFT, "<<") elif self.next_char_is(i, "="): token = Token(TokenType.LE, "<=") else: token = Token(TokenType.LT, c) elif c == ">": if self.next_char_is(i, ">"): token = Token(TokenType.RSHIFT, ">>") elif self.next_char_is(i, "="): token = Token(TokenType.GE, ">=") else: token = Token(TokenType.GT, c) elif c == ".": if self.next_char_is(i, "*"): token = Token(TokenType.DOTSTAR, ".*") elif i + 1 < len(self.string) and self.string[i + 1].isdigit(): token = self.lex_number(i) else: token = Token(TokenType.DOT, c) elif c == '"' or c == "'" or c == "`": token = self.lex_quoted_token(i) else: raise ValueError("Unknown character at {0}".format(i)) self.tokens.append(token) i += token.len def assert_cur_token(self, type): if self.pos >= len(self.tokens): raise ValueError("Expected token type {0} at pos {1} but no " "tokens left".format(type, self.pos)) if self.tokens[self.pos].type != type: raise ValueError("Expected token type {0} at pos {1} but found " "type {2}".format(type, self.pos, self.tokens[self.pos])) def cur_token_type_is(self, type): return self.pos_token_type_is(self.pos, type) def next_token_type_is(self, type): return self.pos_token_type_is(self.pos + 1, type) def pos_token_type_is(self, pos, type): return pos < len(self.tokens) and self.tokens[pos].type == type def consume_token(self, type): self.assert_cur_token(type) v = self.tokens[self.pos].val self.pos = self.pos + 1 return v def paren_expr_list(self): """Parse a paren-bounded expression list for function arguments or IN list and return a list of Expr objects. """ exprs = [] self.consume_token(TokenType.LPAREN) if not self.cur_token_type_is(TokenType.RPAREN): exprs.append(self.expr()) while self.cur_token_type_is(TokenType.COMMA): self.pos = self.pos + 1 exprs.append(self.expr()) self.consume_token(TokenType.RPAREN) return exprs def identifier(self): self.assert_cur_token(TokenType.IDENT) id = Identifier() if self.next_token_type_is(TokenType.DOT): id.schema_name = self.consume_token(TokenType.IDENT) self.consume_token(TokenType.DOT) id.name = self.tokens[self.pos].val self.pos = self.pos + 1 return id def function_call(self): e = Expr() e.type = Expr.FUNC_CALL e.function_call.name.CopyFrom(self.identifier()) e.function_call.param.extend(self.paren_expr_list()) return e def docpath_member(self): self.consume_token(TokenType.DOT) token = self.tokens[self.pos] if token.type == TokenType.IDENT: if token.val.startswith('`') and token.val.endswith('`'): raise ValueError("{0} is not a valid JSON/ECMAScript " "identifier".format(token.value)) self.consume_token(TokenType.IDENT) memberName = token.val elif self.token.type == TokenType.LSTRING: self.consume_token(TokenType.LSTRING) memberName = token.val else: raise ValueError("Expected token type IDENT or LSTRING in JSON " "path at token pos {0}".format(self.pos)) item = DocumentPathItem(type=DocumentPathItem.MEMBER, value=memberName) return item def docpath_array_loc(self): self.consume_token(TokenType.LSQBRACKET) if self.cur_token_type_is(TokenType.MUL): self.consume_token(TokenType.RSQBRACKET) return DocumentPathItem(type=DocumentPathItem.ARRAY_INDEX_ASTERISK) elif self.cur_token_type_is(TokenType.LNUM): v = int(self.consume_token(TokenType.LNUM)) if v < 0: raise IndexError("Array index cannot be negative at {0}" "".format(self.pos)) self.consume_token(TokenType.RSQBRACKET) return DocumentPathItem(type=DocumentPathItem.ARRAY_INDEX, index=v) else: raise ValueError("Exception token type MUL or LNUM in JSON " "path array index at token pos {0}" "".format(self.pos)) def document_field(self): col = ColumnIdentifier() if self.cur_token_type_is(TokenType.IDENT): col.document_path.extend([ DocumentPathItem(type=DocumentPathItem.MEMBER, value=self.consume_token(TokenType.IDENT))]) col.document_path.extend(self.document_path()) return Expr(type=Expr.IDENT, identifier=col) def document_path(self): """Parse a JSON-style document path, like WL#7909, but prefix by @. instead of $. We parse this as a string because the protocol doesn't support it. (yet) """ docpath = [] while True: if self.cur_token_type_is(TokenType.DOT): docpath.append(self.docpath_member()) elif self.cur_token_type_is(TokenType.DOTSTAR): self.consume_token(TokenType.DOTSTAR) docpath.append(DocumentPathItem( type=DocumentPathItem.MEMBER_ASTERISK)) elif self.cur_token_type_is(TokenType.LSQBRACKET): docpath.append(self.docpath_array_loc()) elif self.cur_token_type_is(TokenType.DOUBLESTAR): self.consume_token(TokenType.DOUBLESTAR) docpath.append(DocumentPathItem( type=DocumentPathItem.DOUBLE_ASTERISK)) else: break items = len(docpath) if items > 0 and docpath[items-1].type == \ DocumentPathItem.DOUBLE_ASTERISK: raise ValueError("JSON path may not end in '**' at {0}" "".format(self.pos)) return docpath def column_identifier(self): parts = [] parts.append(self.consume_token(TokenType.IDENT)) while self.cur_token_type_is(TokenType.DOT): self.consume_token(TokenType.DOT) parts.append(self.consume_token(TokenType.IDENT)) if len(parts) > 3: raise ValueError("Too many parts to identifier at {0}" "".format(self.pos)) parts.reverse() colid = ColumnIdentifier() # clever way to apply them to the struct for i in xrange(0, len(parts)): if i == 0: colid.name = parts[0] elif i == 1: colid.table_name = parts[1] elif i == 2: colid.schema_name = parts[2] if self.cur_token_type_is(TokenType.DOLLAR): self.consume_token(TokenType.DOLLAR) colid.document_path = self.document_path() e = Expr() e.type = Expr.IDENT e.identifier.CopyFrom(colid) return e def next_token(self): if (self.pos >= len(self.tokens)): raise ValueError("Unexpected end of token stream") t = self.tokens[self.pos] self.pos += 1 return t def expect_token(self, token_type): t = self.next_token() if t.type != token_type: raise ValueError("Expected token type {0}".format(token_type)) def parse_json_doc(self): o = Object() def parse_place_holder(self, token): place_holder_name = "" if self.cur_token_type_is(TokenType.LNUM): place_holder_name = self.consume_token(TokenType.LNUM_INT) elif self.cur_token_type_is(TokenType.IDENT): place_holder_name = self.consume_token(TokenType.IDENT) elif token.type == TokenType.EROTEME: place_holder_name = str(self.positional_placeholder_count) else: raise ValueError("Invalid placeholder name at token pos {0}" "".format(self.pos)) place_holder_name = place_holder_name.lower() expr = Expr(type=Expr.PLACEHOLDER) if place_holder_name in self.placeholder_name_to_position: expr.position = \ self.placeholder_name_to_position[place_holder_name] else: expr.position = self.positional_placeholder_count self.placeholder_name_to_position[place_holder_name] = \ self.positional_placeholder_count self.positional_placeholder_count += 1 return expr def atomic_expr(self): """Parse an atomic expression and return a protobuf Expr object""" token = self.next_token() if token.type in [TokenType.EROTEME, TokenType.COLON]: return self.parse_place_holder(token) elif token.type == TokenType.LCURLY: return self.parse_json_doc() elif token.type == TokenType.CAST: # TODO implement pass pass elif token.type == TokenType.LPAREN: e = self.expr() self.expect_token(TokenType.RPAREN) return e elif token.type in [TokenType.PLUS, TokenType.MINUS]: pass elif token.type in [TokenType.PLUS, TokenType.MINUS]: peek = self.peek_token() if peek.type == TokenType.LNUM: self.tokens[self.pos].val = token.val + peek.val return self.atomic_expr() return build_unary_op(token.val, self.atomic_expr()) elif token.type in [TokenType.NOT, TokenType.NEG, TokenType.BANG]: return build_unary_op(token.val, self.atomic_expr()) elif token.type == TokenType.LSTRING: return build_literal_expr(build_string_scalar(token.val)) elif token.type == TokenType.NULL: return build_literal_expr(build_null_scalar()) elif token.type == TokenType.LNUM: if "." in token.val: return build_literal_expr( build_double_scalar(float(token.val))) else: return build_literal_expr(build_int_scalar(int(token.val))) elif token.type in [TokenType.TRUE, TokenType.FALSE]: return build_literal_expr( build_bool_scalar(token.type == TokenType.TRUE)) elif token.type == TokenType.DOLLAR: return self.document_field() elif token.type == TokenType.MUL: return self.starOperator() elif token.type == TokenType.IDENT: self.pos = self.pos - 1 # stay on the identifier if self.next_token_type_is(TokenType.LPAREN) or \ (self.next_token_type_is(TokenType.DOT) and self.pos_token_type_is(self.pos + 2, TokenType.IDENT) and self.pos_token_type_is(self.pos + 3, TokenType.LPAREN)): # Function call return self.function_call() else: return (self.document_field() if not self._allowRelationalColumns else self.column_identifier()) # if t.type == TokenType.EROTEME: # return build_literal_expr(build_string_scalar("?")) # elif t.type == TokenType.INTERVAL: # e = Expr() # e.type = Expr.OPERATOR # e.operator.name = "INTERVAL" # e.operator.param.add().CopyFrom(self.expr()) # # validate the interval units # if self.pos < len(self.tokens) and self.tokens[self.pos].type in interval_units: # pass # else: # raise StandardError("Expected interval units at " + str(self.pos)) # e.operator.param.add().CopyFrom(build_literal_expr(build_string_scalar(self.tokens[self.pos].val))) # self.pos = self.pos + 1 # return e raise ValueError("Unknown token type = {0} when expecting atomic " "expression at {1}".format(token.type, self.pos)) def parse_left_assoc_binary_op_expr(self, types, inner_parser): """Given a `set' of types and an Expr-returning inner parser function, parse a left associate binary operator expression""" lhs = inner_parser() while (self.pos < len(self.tokens) and self.tokens[self.pos].type in types): e = Expr() e.type = Expr.OPERATOR e.operator.name = self.tokens[self.pos].val e.operator.param.add().CopyFrom(lhs) self.pos = self.pos + 1 e.operator.param.add().CopyFrom(inner_parser()) lhs = e return lhs # operator precedence is implemented here def mul_div_expr(self): return self.parse_left_assoc_binary_op_expr( set([TokenType.MUL, TokenType.DIV, TokenType.MOD]), self.atomic_expr) def add_sub_expr(self): return self.parse_left_assoc_binary_op_expr( set([TokenType.PLUS, TokenType.MINUS]), self.mul_div_expr) def shift_expr(self): return self.parse_left_assoc_binary_op_expr( set([TokenType.LSHIFT, TokenType.RSHIFT]), self.add_sub_expr) def bit_expr(self): return self.parse_left_assoc_binary_op_expr( set([TokenType.BITAND, TokenType.BITOR, TokenType.BITXOR]), self.shift_expr) def comp_expr(self): return self.parse_left_assoc_binary_op_expr( set([TokenType.GE, TokenType.GT, TokenType.LE, TokenType.LT, TokenType.EQ, TokenType.NE]), self.bit_expr) def ilri_expr(self): lhs = self.comp_expr() is_not = False if self.cur_token_type_is(TokenType.NOT): is_not = True self.consume_token(TokenType.NOT) if self.pos < len(self.tokens): params = [lhs] op_name = self.tokens[self.pos].val if self.cur_token_type_is(TokenType.IS): self.consume_token(TokenType.IS) # for IS, NOT comes AFTER if self.cur_token_type_is(TokenType.NOT): is_not = True self.consume_token(TokenType.NOT) params.append(self.comp_expr()) elif self.cur_token_type_is(TokenType.IN): self.consume_token(TokenType.IN) params.extend(self.paren_expr_list()) elif self.cur_token_type_is(TokenType.LIKE): self.consume_token(TokenType.LIKE) params.append(self.comp_expr()) if self.cur_token_type_is(TokenType.ESCAPE): self.consume_token(TokenType.ESCAPE) params.append(self.comp_expr()) elif self.cur_token_type_is(TokenType.BETWEEN): self.consume_token(TokenType.BETWEEN) params.append(self.comp_expr()) self.consume_token(TokenType.AND) params.append(self.comp_expr()) elif self.cur_token_type_is(TokenType.REGEXP): self.consume_token(TokenType.REGEXP) params.append(self.comp_expr()) else: if is_not: raise ValueError("Unknown token after NOT as pos {0}" "".format(self.pos)) op_name = None # not an operator we're interested in if op_name: e = Expr() e.type = Expr.OPERATOR e.operator.name = op_name e.operator.param.extend(params) if is_not: # wrap if `NOT'-prefixed e = build_unary_op("NOT", e) lhs = e return lhs def and_expr(self): return self.parse_left_assoc_binary_op_expr( set([TokenType.AND, TokenType.ANDAND]), self.ilri_expr) def or_expr(self): return self.parse_left_assoc_binary_op_expr( set([TokenType.OR, TokenType.OROR]), self.and_expr) def expr(self): return self.or_expr() def parse_table_insert_field(self): return Column(name=self.consume_token(TokenType.IDENT)) def parse_table_update_field(self): return self.column_identifier().identifier def parse_table_select_projection(self): project_expr = [] first = True while self.pos < len(self.tokens): if not first: self.consume_token(TokenType.COMMA) first = False projection = Projection(source=self.expr()) if self.cur_token_type_is(TokenType.AS): self.consume_token(TokenType.AS) projection.alias = self.consume_token(TokenType.IDENT) else: self.pos -= 1 projection.alias = self.consume_token(TokenType.IDENT) project_expr.append(projection) return project_expr def parse_order_spec(self): order_specs = [] first = True while self.pos < len(self.tokens): if not first: self.consume_token(TokenType.COMMA) first = False order = Order(expr=self.expr()) if self.cur_token_type_is(TokenType.ORDERBY_ASC): order.direction = Order.ASC self.consume_token(TokenType.ORDERBY_ASC) elif self.cur_token_type_is(TokenType.ORDERBY_DESC): order.direction = Order.DESC self.consume_token(TokenType.ORDERBY_DESC) order_specs.append(order) return order_specs def parse_expr_list(self): expr_list = [] first = True while self.pos < len(self.tokens): if not first: self.consume_token(TokenType.COMMA) first = False expr_list.append(self.expr()) return expr_list def parseAndPrintExpr(expr_string, allowRelational=True): print(">>>>>>> parsing: {0}".format(expr_string)) p = ExprParser(expr_string, allowRelational) print(p.tokens) e = p.expr() print(e) def x_test(): parseAndPrintExpr("name like :name") return parseAndPrintExpr("10+1") parseAndPrintExpr("(abc == 1)") parseAndPrintExpr("(func(abc)=1)") parseAndPrintExpr("(abc = \"jess\")") parseAndPrintExpr("(abc = \"with \\\"\")") parseAndPrintExpr("(abc != .10)") parseAndPrintExpr("(abc != \"xyz\")") parseAndPrintExpr("a + b * c + d") parseAndPrintExpr("(a + b) * c + d") parseAndPrintExpr("(field not in ('a',func('b', 2.0),'c'))") parseAndPrintExpr("jess.age between 30 and death") parseAndPrintExpr("a + b * c + d") parseAndPrintExpr("x > 10 and Y >= -20") parseAndPrintExpr("a is true and b is null and C + 1 > 40 and " "(time = now() or hungry())") parseAndPrintExpr("a + b + -c > 2") parseAndPrintExpr("now () + b + c > 2") parseAndPrintExpr("now () + @b + c > 2") parseAndPrintExpr("now () - interval +2 day > some_other_time() or " "something_else IS NOT NULL") parseAndPrintExpr("\"two quotes to one\"\"\"") parseAndPrintExpr("'two quotes to one'''") parseAndPrintExpr("'different quote \"'") parseAndPrintExpr("\"different quote '\"") parseAndPrintExpr("`ident`") parseAndPrintExpr("`ident```") parseAndPrintExpr("`ident\"'`") parseAndPrintExpr("now () - interval -2 day") parseAndPrintExpr("? > x and func(?, ?, ?)") parseAndPrintExpr("a > now() + interval (2 + x) MiNuTe") parseAndPrintExpr("a between 1 and 2") parseAndPrintExpr("a not between 1 and 2") parseAndPrintExpr("a in (1,2,a.b(3),4,5,x)") parseAndPrintExpr("a not in (1,2,3,4,5,@x)") parseAndPrintExpr("a like b escape c") parseAndPrintExpr("a not like b escape c") parseAndPrintExpr("(1 + 3) in (3, 4, 5)") parseAndPrintExpr("`a crazy \"function\"``'name'`(1 + 3) in (3, 4, 5)") parseAndPrintExpr("`a crazy \"function\"``'name'`(1 + 3) in (3, 4, 5)") parseAndPrintExpr("a@.b", False) parseAndPrintExpr("a@.b[0][0].c**.d.\"a weird\\\"key name\"", False) parseAndPrintExpr("a@.*", False) parseAndPrintExpr("a@[0].*", False) parseAndPrintExpr("a@**[0].*", False) # x_test()