使用pyparsing分析带有带括号谓词的嵌套SQL查询



我正在尝试解析包含带括号谓词的形式的嵌套查询。示例:

query = '(A LIKE "%.something.com" AND B = 4) OR (C In ("a", "b") AND D Contains "asdf")'

我已经尝试了我看到的许多答案/例子,但没有让它们发挥作用,这就是我到目前为止想出的几乎(?)有效的答案/例子:

from pyparsing import *
r = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'*+,-./:;<=>?@[]^_`{|}~'
any_keyword = CaselessKeyword("AND") | CaselessKeyword("OR")
non_keyword = ~any_keyword + Word(r)
expr = infixNotation(originalTextFor(non_keyword[1, ...]),
[
(oneOf("AND OR", caseless=True, asKeyword=True), 2, opAssoc.LEFT)
])

然后,运行expr.parseString(query).asList()只返回:

[['A LIKE "%.something.com"', 'AND', 'B = 4']]

而没有查询的其余部分。据我所知,这是由于C In ("a", "b")部分,因为那里有括号。

有没有办法";无视";谓词中的括号,以便解析返回预期的答案:

[[['A LIKE "%.something.com"', 'AND', 'B = 4'], 'OR', ['C In ("a", "b")', 'AND', 'D Contains "asdf"']]]

欢迎使用pyparsing!您已经从以下其他示例中取得了一些不错的进展,但让我们后退一点。

infixNotation绝对是正确的方法,但这个表达式中的运算符比ANDOR多得多。这些都是输入字符串中的子表达式:

A LIKE "%.something.com"
B = 4
C in ("a", "b")
D contains "asdf"

每一个都是它自己的二进制表达式;LIKE"&"在";以及";包含";。此外,您的操作数不仅可以是标识符,还可以是带引号的字符串、带引号字符串的集合和数字。

我喜欢你的直觉,这是一个术语的逻辑表达,所以让我们定义两个级别的infixNotations:

  1. 列或数字"的表达式;算术";(使用"="、"LIKE"等)
  2. 由#1定义的术语表达式,结合逻辑NOT、AND和OR运算符

如果我们将#1称为column_expr,#2看起来与您已经写的类似:

expr = infixNotation(column_expr,
[
(NOT, 1, opAssoc.RIGHT),
(AND, 2, opAssoc.LEFT),
(OR, 2, opAssoc.LEFT),
])

我添加了NOT作为对您现在拥有的内容的合理扩展——大多数逻辑表达式都包括这3个运算符。此外,传统的是以这种优先顺序来定义它们;X或Y与Z";最终按照"0"的顺序进行评估;"X或(Y和Z)";,因为AND具有比OR更高的优先级(而NOT仍然更高)。

#1需要更多的工作,所以我已经为column_expr的单个操作数编写了一个小的BNF(我不建议采取足够高的步骤!):

identifier ::= one or more Words composed of printable letters (we may come back to this)
number ::= an integer or real number (we can use the one defined in pyparsing_common.number)
quotedString ::= (a quoted string like one define by pyparsing)
quotedString_list ::= '(' quotedString [',' quotedString]... ')'
# put identifier last, since it will match just about anything, and we want to try the other
# expressions first
column_operand ::= quotedString | quotedString_list | number | identifier

然后,column_expr将是使用以下column_operands:的infixNotation

ppc = pyparsing_common
LPAR, RPAR = map(Suppress, "()")
# use Group to keep these all together
quotedString_list = Group(LPAR + delimitedList(quotedString) + RPAR)
column_operand = quotedString | quotedString_list | ppc.number | identifier
column_expr = infixNotation(column_operand,
[
(IN | CONTAINS, 2, opAssoc.LEFT),
(LIKE, 2, opAssoc.LEFT),
('=', 2, opAssoc.LEFT),
])

如果你发现你必须添加其他运算符,比如"&";,很可能您会将它们添加到column_expr中。

其他注意事项:

  • 您可能想要删除"one_answers";来自r,因为它们实际上应该作为引用字符串的一部分来处理

  • 随着关键字列表的增长,您会发现使用以下方法更容易定义它们:

    AND, OR, NOT, LIKE, IN, CONTAINS = keyword_exprs = list(map(CaselessKeyword, """
    AND OR NOT LIKE IN CONTAINS
    """.split()))
    any_keyword = MatchFirst(keyword_exprs)
    

    然后你可以更容易地引用它们,就像我在上面的代码中所做的那样。

  • 在尝试测试您发布的复杂查询之前,请先编写小测试。在包含操作数的许多变体方面做得很好。然后使用runTests运行它们,如下所示:

    expr.runTests("""
    A LIKE "%.something.com"
    B = 4
    C in ("A", "B")
    D CONTAINS "ASDF"
    (A LIKE "%.something.com" AND B = 4) OR (C In ("a", "b") AND D Contains "asdf")
    """)
    

有了这些更改,我为您的原始查询字符串得到了这个:

[[['A', 'LIKE', '"%.something.com"'], 'AND', 'B = 4'], 'OR', [['C', 'IN', ['"a"', '"b"']], 'AND', ['D', 'CONTAINS', '"asdf"']]]

嗯,我不喜欢一个看起来像'B = 4'的术语,现在我们实际上正在解析子表达式。我怀疑这是因为您对标识符的定义有点过于激进。如果我们将其缩减为仅~any_keyword + Word(alphas, r),强制使用前导字母字符,而不使用[1, ...]进行重复,那么我们会得到更好的外观:

[[['A', 'LIKE', '"%.something.com"'], 'AND', ['B', '=', 4]], 'OR', [['C', 'IN', ['"a"', '"b"']], 'AND', ['D', 'CONTAINS', '"asdf"']]]

事实上,如果确实希望这些子表达式保留在原始表达式中,并在逻辑运算符上分解,那么您可以像以前一样将column_expr封装在originalTextFor中,给出:

[['A LIKE "%.something.com"', 'AND', 'B = 4'], 'OR', ['C In ("a", "b")', 'AND', 'D Contains "asdf"']]

祝你的SQL解析项目好运!

最新更新