过滤sqlalchemy加入查询,在解析输入上构造

时间:2014-12-17 18:40:34

标签: python sqlalchemy pyparsing

我坚持这个:我已经采用了bauble (a program on github),其中一部分是为了在sql数据库上指定一个查询。查询语言实际上是三种不同的语言,其中一种语言(在SQL查询中进行过滤)我正在重写。

原作者选择了pyparsing,我没有理由审查这个选择,除了我不知道pyparsing,我总是和lex和yacc玩得很开心......但我决定继续pyparsing,所以我和#39;我正在学习它。

我(重新)编写了一个识别给定查询的解析器,大多数语法类别都转换为类。我认为解析部分非常好,我遇到的问题就是我用pyparsing创建的对象需要使用SQLAlchemy来查询数据库,特别是当我根据属性进行过滤时来自联合表。

语法的相关部分,采用pyparsing格式:

query_expression = Forward()
identifier = Group(delimitedList(Word(alphas, alphanums+'_'),
                                 '.')).setParseAction(IdentifierToken)
ident_expression = (
    Group(identifier + binop + value).setParseAction(IdentExpressionToken)
    | (
        Literal('(') + query_expression + Literal(')')
    ).setParseAction(ParenthesisedQuery))
query_expression << infixNotation(
    ident_expression,
    [ (NOT_, 1, opAssoc.RIGHT, SearchNotAction),
      (AND_, 2, opAssoc.LEFT,  SearchAndAction),
      (OR_,  2, opAssoc.LEFT,  SearchOrAction) ] )

和相应的类(最后两个的evaluate方法是我还不知道怎么写的):

class BinaryLogical(object):
    ## abstract base class. `name` is defined in derived classes
    def __init__(self, t):
        self.op = t[0][1]
        self.operands = t[0][0::2]  # every second object is an operand

    def __repr__(self):
        return "(%s %s %s)" % (self.operands[0], self.name, self.operands[1])


class SearchAndAction(BinaryLogical):
    name = 'AND'

    def evaluate(self, domain, session):
        return self.operands[0].evaluate(domain, session).intersect_all(
            map(lambda i: i.evaluate(domain, session), self.operands[1:]))


class SearchOrAction(BinaryLogical):
    name = 'OR'

    def evaluate(self, domain, session):
        return self.operands[0].evaluate(domain, session).union_all(
            map(lambda i: i.evaluate(domain, session), self.operands[1:]))


class SearchNotAction(object):
    name = 'NOT'

    def __init__(self, t):
        self.op, self.operand = t[0]

    def evaluate(self, domain, session):
        return session.query(domain).except_(self.operand.evaluate(domain, session))

    def __repr__(self):
        return "%s %s" % (self.name, str(self.operand))



class ParenthesisedQuery(object):
    def __init__(self, t):
        self.query = t[1]

    def __repr__(self):
        return "(%s)" % self.query.__repr__()

    def evaluate(self, domain, session):
        return self.query.evaluate(domain, session)


class IdentifierToken(object):
    def __init__(self, t):
        self.value = t[0]

    def __repr__(self):
        return '.'.join(self.value)

    def evaluate(self, domain, session):
        q = session.query(domain)
        if len(self.value) > 1:
            q = q.join(self.value[:-1], aliased=True)
        return q.subquery().c[self.value[-1]]


class IdentExpressionToken(object):
    def __init__(self, t):
        self.op = t[0][1]
        self.operation = {'>': lambda x,y: x>y,
                          '<': lambda x,y: x<y,
                          '>=': lambda x,y: x>=y,
                          '<=': lambda x,y: x<=y,
                          '=': lambda x,y: x==y,
                          '!=': lambda x,y: x!=y,
                      }[self.op]
        self.operands = t[0][0::2]  # every second object is an operand

    def __repr__(self):
        return "(%s %s %s)" % ( self.operands[0], self.op, self.operands[1])

    def evaluate(self, domain, session):
        return session.query(domain).filter(self.operation(self.operands[0].evaluate(domain, session),
                                                           self.operands[1].express()))

上述代码段的完整且最新的代码为here

几个可能的查询:

results = mapper_search.search("plant where accession.species.id=44")
results = mapper_search.search("species where genus.genus='Ixora'")
results = mapper_search.search("species where genus.genus=Maxillaria and not genus.family=Orchidaceae")

2 个答案:

答案 0 :(得分:0)

似乎以前的开发人员在创建这些类时遇到了很多麻烦 - 这实际上是使用pyparsing时的“最佳实践”。目的是这些类作为解析过程的输出,通常使用解析的元素支持它们自己的一些行为。在这种情况下,元素也可以通过名称访问(另一个pyparsing“最佳实践”)。一旦在解析过程中构造了这些类,pyparsing几乎不在图片中 - 任何进一步的处理纯粹是这些类的函数。

我认为目标可能和你假设的一样,这些类有results.statement.invoke()这样的方法。查看这些类的方法并查看它们为您提供的内容,尤其是顶级StatementAction类。如果没有这样的方法,那么可能是下一步,您可以以对SQLAlchemy数据库包装器有意义的方式应用解析的值。

答案 1 :(得分:0)

我想我找到了一个暂时可以接受的答案,但它使用了来自SQLAlchemy的内部信息(下划线前缀字段)。

问题的核心是,由于我正在使用来自用户的解析信息,我开始使用看起来像类的名称,以及名称导航的关系。例如,在plant where accession.species.id=44中,类名称为Plant,我正在对连接的id对象的Species进行过滤。

上面的例子可能会让人觉得事情很容易,只是资本化问题。但我们仍然需要知道在哪个模块中找到PlantAccessionSpecies

另一个例子:family where genera.id!=0。通常,关系的名称不需要等于所引用的类的名称。

语法还可以,我不需要进一步修改它。在与SQLAlchemy的交互中,这一点是(现在仍然是部分),因此我必须更正类evaluateIdentifierToken中的IdentExpressionToken方法。

我的解决方案包含以下代码:

class IdentifierToken(object):
....
    def evaluate(self, env):
        """return pair (query, attribute)

        the value associated to the identifier is an altered query where the
        joinpoint is the one relative to the attribute, and the attribute
        itself.
        """

        query = env.session.query(env.domain)
        if len(self.value) == 1:
            # identifier is an attribute of the table being queried
            attr = getattr(env.domain, self.value[0])
        elif len(self.value) > 1:
            # identifier is an attribute of a joined table
            query = query.join(*self.value[:-1], aliased=True)
            attr = getattr(query._joinpoint['_joinpoint_entity'], self.value[-1])
        return query, attr

class IdentExpressionToken(object):
...
    def evaluate(self, env):
        q, a = self.operands[0].evaluate(env)
        clause = lambda x: self.operation(a, x)
        return q.filter(clause(self.operands[1].express()))

一些观点:

  • 我最初不清楚查询方法没有改变调用它的查询,但是我必须使用返回的值。
  • 我对连接的查询进行别名处理,以便轻松检索连接操作的“目标”类。
  • 别名加入查询的
  • ,我正在使用字段_joinpoint,它看起来像非公开信息。
  • query._joinpoint['_joinpoint_entity']是对我需要检索已解析查询中指定的字段的类的引用。 _joinpoint字典在非别名查询上看起来不同。

  • 问题仍然是开放部分是否有“官方”SQLAlchemy方法来检索此信息。