From 52176fe78556cab07b3c823e9454029a1e15271c Mon Sep 17 00:00:00 2001 From: "James E. Blair" <jeblair@hp.com> Date: Mon, 12 Jan 2015 11:45:14 +1300 Subject: [PATCH] Search: join tables when necessary Some searches (eg, "owner:foo" or "project:bar") were performing an implicit join on the primary key that joins them with the change table in a 1:1 relationship. If a user negated those, then the result would be "not (a.key=b.key and a.value=foo)" which is equal to "a.key!=b.key or a.value!=foo". The partial negation of the join criteria is definitely not what we want. Instead, for the known 1:1 relationships that can be produced, add the join criteria outside of the main expression iff those tables are referenced. Then the remaining part of the condition (a.value == foo) can be optionally negated as normal. Expressions for relationships other than 1:1 are handled by subselects and so should not be impacted by this. Also, I'm leaving a simple bit of debug code in the search module for future use. Change-Id: I92bb0a01f1768cb4f7cc9b3b28d54ebd309fa406 --- gertty/search/__init__.py | 39 ++++++++++++++++++++++++++++++++++++++- gertty/search/parser.py | 13 +++++-------- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/gertty/search/__init__.py b/gertty/search/__init__.py index 5837a05..7902970 100644 --- a/gertty/search/__init__.py +++ b/gertty/search/__init__.py @@ -13,6 +13,7 @@ # under the License. import sqlalchemy.sql.expression +from sqlalchemy.sql.expression import and_ from gertty.search import tokenizer, parser import gertty.db @@ -28,6 +29,42 @@ class SearchCompiler(object): self.lexer = tokenizer.SearchTokenizer() self.parser = parser.SearchParser() + def findTables(self, expression): + tables = set() + stack = [expression] + while stack: + x = stack.pop() + if hasattr(x, 'table'): + if (x.table != gertty.db.change_table + and hasattr(x.table, 'name')): + tables.add(x.table) + for child in x.get_children(): + if not isinstance(child, sqlalchemy.sql.selectable.Select): + stack.append(child) + return tables + def parse(self, data): self.parser.username = self.app.config.username - return self.parser.parse(data, lexer=self.lexer) + result = self.parser.parse(data, lexer=self.lexer) + tables = self.findTables(result) + if gertty.db.project_table in tables: + result = and_(gertty.db.change_table.c.project_key == gertty.db.project_table.c.key, + result) + tables.remove(gertty.db.project_table) + if gertty.db.account_table in tables: + result = and_(gertty.db.change_table.c.account_key == gertty.db.account_table.c.key, + result) + tables.remove(gertty.db.account_table) + if tables: + raise Exception("Unknown table in search: %s" % tables) + return result + +if __name__ == '__main__': + class Dummy(object): + pass + app = Dummy() + app.config = Dummy() + app.config.username = 'bob' + search = SearchCompiler(app) + x = search.parse('owner:self') + print x diff --git a/gertty/search/parser.py b/gertty/search/parser.py index 0b747a2..5f680fa 100644 --- a/gertty/search/parser.py +++ b/gertty/search/parser.py @@ -122,13 +122,11 @@ def SearchParser(): '''owner_term : OP_OWNER string''' if p[2] == 'self': username = p.parser.username - p[0] = and_(gertty.db.change_table.c.account_key == gertty.db.account_table.c.key, - gertty.db.account_table.c.username == username) + p[0] = gertty.db.account_table.c.username == username else: - p[0] = and_(gertty.db.change_table.c.account_key == gertty.db.account_table.c.key, - or_(gertty.db.account_table.c.username == p[2], - gertty.db.account_table.c.email == p[2], - gertty.db.account_table.c.name == p[2])) + p[0] = or_(gertty.db.account_table.c.username == p[2], + gertty.db.account_table.c.email == p[2], + gertty.db.account_table.c.name == p[2]) def p_reviewer_term(p): '''reviewer_term : OP_REVIEWER string''' @@ -266,8 +264,7 @@ def SearchParser(): elif p[2] == 'abandoned': p[0] = gertty.db.change_table.c.status == 'ABANDONED' elif p[2] == 'owner': - p[0] = and_(gertty.db.change_table.c.account_key == gertty.db.account_table.c.key, - gertty.db.account_table.c.username == username) + p[0] = gertty.db.account_table.c.username == username elif p[2] == 'reviewer': filters = [] filters.append(gertty.db.approval_table.c.change_key == gertty.db.change_table.c.key)