diff --git a/mo_sql_parsing/keywords.py b/mo_sql_parsing/keywords.py index fc8d20e..40df99f 100644 --- a/mo_sql_parsing/keywords.py +++ b/mo_sql_parsing/keywords.py @@ -124,9 +124,19 @@ ) REGEXP = keyword("regexp").set_parser_name("rgx") NEQ = (Literal("!=") | Literal("<>")).set_parser_name("neq") -LAMBDA = Literal("->").set_parser_name("lambda") ASSIGN = Literal(":=").set_parser_name("assign") +JSON_GET = Literal("->").set_parser_name("json_get") +JSON_GET_TEXT = Literal("->>").set_parser_name("json_get_text") +JSON_PATH = Literal("#>").set_parser_name("json_path") +JSON_PATH_TEXT = Literal("#>>").set_parser_name("json_path_text") +JSON_SUBSUMES = Literal("@>").set_parser_name("json_subsumes") +JSON_SUBSUMED_BY = Literal("<@").set_parser_name("json_subsumed_by") +JSON_CONTAINS = Literal("?").set_parser_name("json_contains") +JSON_CONTAINS_ANY = Literal("?|").set_parser_name("json_contains_any") +JSON_CONTAINS_ALL = Literal("?&").set_parser_name("json_contains_all") +JSON_PATH_DEL = Literal("#-").set_parser_name("json_path_del") + AND = keyword("and") APPLY = keyword("apply") BEGIN = keyword("begin").suppress() @@ -290,6 +300,16 @@ "validate_conversion": 0, "collate": 0, "concat": 1, + "json_get": 1.5, + "json_get_text": 1.5, + "json_path": 1.5, + "json_path_text": 1.5, + "json_subsumes": 1.5, + "json_subsumed_by": 1.5, + "json_contains": 1.5, + "json_contains_any": 1.5, + "json_contains_all": 1.5, + "json_path_del": 1.5, "mul": 2, "div": 1.5, "mod": 2, @@ -351,6 +371,16 @@ KNOWN_OPS = [ COLLATE, CONCAT, + JSON_GET_TEXT + | JSON_GET + | JSON_PATH_TEXT + | JSON_PATH + | JSON_SUBSUMES + | JSON_SUBSUMED_BY + | JSON_CONTAINS_ANY + | JSON_CONTAINS_ALL + | JSON_CONTAINS + | JSON_PATH_DEL, POS, NEG, MUL | DIV | MOD, @@ -378,7 +408,6 @@ NOT, AND, OR, - LAMBDA, ASSIGN, REGEXP, NOT_REGEXP, diff --git a/mo_sql_parsing/sql_parser.py b/mo_sql_parsing/sql_parser.py index 68cf65c..f0a790a 100644 --- a/mo_sql_parsing/sql_parser.py +++ b/mo_sql_parsing/sql_parser.py @@ -328,6 +328,10 @@ def matching(type): + ZeroOrMore(Group(simple_accessor | dynamic_accessor)) ) + _lambda = Group( + LB + Group(delimited_list(identifier))("params") + RB + Literal("->").suppress() + expression("lambda") + ) + with NO_WHITESPACE: def scale(tokens): @@ -355,6 +359,7 @@ def scale(tokens): | create_array | create_map | create_struct + | _lambda | (LB + Group(query) + RB) | (LB + Group(delimited_list(expression)) / to_tuple_call + RB) | literal_string @@ -386,7 +391,7 @@ def scale(tokens): o, 1 if o in unary_ops else (3 if isinstance(o, tuple) else 2), unary_ops.get(o, LEFT_ASSOC), - to_lambda if o is LAMBDA else to_json_operator, + to_json_operator, ) for o in KNOWN_OPS ], @@ -769,7 +774,7 @@ def mult(tokens): drops = assign( "drop", temporary - +MatchFirst([ + + MatchFirst([ keyword(item).suppress() + Optional(flag("if exists")) + Group(identifier)(item) for item in ["table", "view", "index", "schema"] ]), @@ -1055,16 +1060,8 @@ def mult(tokens): + keyword("end if").suppress() ) leave = assign("leave", identifier) - while_block = ( - assign("while", expression) - + assign("do", many_command) - + keyword("end while").suppress() - ) - loop_block = ( - keyword("loop").suppress() - + many_command("loop") - + keyword("end loop").suppress() - ) + while_block = assign("while", expression) + assign("do", many_command) + keyword("end while").suppress() + loop_block = keyword("loop").suppress() + many_command("loop") + keyword("end loop").suppress() create_trigger = assign( "create trigger", @@ -1134,25 +1131,16 @@ def mult(tokens): )("declare_handler") declare_cursor = Group( - keyword("declare").suppress() - + identifier("name") - + keyword("cursor for").suppress() - + query("query") + keyword("declare").suppress() + identifier("name") + keyword("cursor for").suppress() + query("query") )("declare_cursor") - transact = ( Group(keyword("start transaction")("op")) / to_json_call | Group(keyword("commit")("op")) / to_json_call | Group(keyword("rollback")("op")) / to_json_call ) - blocks = Group(Optional(identifier("label") + ":") + ( - block - | if_block - | while_block - | loop_block - )) + blocks = Group(Optional(identifier("label") + ":") + (block | if_block | while_block | loop_block)) ############################################################# # FINALLY ASSEMBLE THE PARSER diff --git a/mo_sql_parsing/types.py b/mo_sql_parsing/types.py index a23a6c3..c59d43b 100644 --- a/mo_sql_parsing/types.py +++ b/mo_sql_parsing/types.py @@ -116,6 +116,7 @@ MAP_TYPE = (keyword("map")("op") + LB + delimited_list(simple_types("params")) + RB) / to_json_call ARRAY_TYPE = (keyword("array")("op") + LB + simple_types("params") + RB) / to_json_call JSON = Group(keyword("json")("op")) / to_json_call +JSONB = Group(keyword("jsonb")("op")) / to_json_call DATE = keyword("date") DATETIME = keyword("datetime") @@ -171,6 +172,7 @@ INT64, BYTEINT, JSON, + JSONB, NCHAR, NVARCHAR, NUMBER, diff --git a/mo_sql_parsing/utils.py b/mo_sql_parsing/utils.py index 03d7213..dc0aed4 100644 --- a/mo_sql_parsing/utils.py +++ b/mo_sql_parsing/utils.py @@ -120,11 +120,6 @@ def _chunk(values, size): yield acc -def to_lambda(tokens): - params, op, expr = list(tokens) - return Call("lambda", [expr], {"params": list(params)}) - - def to_json_operator(tokens): # ARRANGE INTO {op: params} FORMAT length = len(tokens.tokens) @@ -242,6 +237,16 @@ def to_tuple_call(token, index, string): "COLLATE": "collate", ":": "get", "||": "concat", + "->": "json_get", + "->>": "json_get_text", + "#>": "json_path", + "#>>": "json_path_text", + "@>": "json_subsumes", + "<@": "json_subsumed_by", + "?": "json_contains", + "?|": "json_contains_any", + "?&": "json_contains_all", + "#-": "json_path_del", "*": "mul", "/": "div", "%": "mod", @@ -279,7 +284,6 @@ def to_tuple_call(token, index, string): "not_simlilar_to": "not_similar_to", "or": "or", "and": "and", - "->": "lambda", ":=": "assign", "union": "union", "union_all": "union_all", diff --git a/packaging/setup.py b/packaging/setup.py index 9f1e671..f6f3395 100644 --- a/packaging/setup.py +++ b/packaging/setup.py @@ -15,6 +15,6 @@ name='mo-sql-parsing', packages=["mo_sql_parsing"], url='https://github.com/klahnakoski/mo-sql-parsing', - version='10.642.24144', + version='10.643.24144', zip_safe=True ) \ No newline at end of file diff --git a/packaging/setuptools.json b/packaging/setuptools.json index 923cf89..9b50ca2 100644 --- a/packaging/setuptools.json +++ b/packaging/setuptools.json @@ -311,6 +311,6 @@ "name": "mo-sql-parsing", "packages": ["mo_sql_parsing"], "url": "https://github.com/klahnakoski/mo-sql-parsing", - "version": "10.642.24144", + "version": "10.643.24144", "zip_safe": true } \ No newline at end of file diff --git a/tests/test_postgres.py b/tests/test_postgres.py index 369dddd..948b4c4 100644 --- a/tests/test_postgres.py +++ b/tests/test_postgres.py @@ -529,3 +529,18 @@ def test_issue_175_extract_millennium(self): result = parse(sql) expected = {"select": {"value": {"extract": ["millennium", "date"]}}} self.assertEqual(result, expected) + + def test_issue_239_jsonb1(self): + sql = """select jsonb ->> 'field_key' FROM a""" + result = parse(sql) + expected = {"from": "a", "select": {"value": {"json_get_text": ["jsonb", {"literal": "field_key"}]}}} + self.assertEqual(result, expected) + + def test_issue_239_jsonb2(self): + sql = """select name::jsonb ->> 'field_key' FROM a""" + result = parse(sql) + expected = { + "from": "a", + "select": {"value": {"json_get_text": [{"cast": ["name", {"jsonb": {}}]}, {"literal": "field_key"}]}}, + } + self.assertEqual(result, expected) diff --git a/tests/test_sqlglot.py b/tests/test_sqlglot.py index dc3b59f..2638815 100644 --- a/tests/test_sqlglot.py +++ b/tests/test_sqlglot.py @@ -13,6 +13,8 @@ from unittest import skip, TestCase +from mo_parsing.debug import Debugger + from mo_sql_parsing import parse