Skip to content

Commit

Permalink
Merge pull request #693 from usc-isi-i2/dev
Browse files Browse the repository at this point in the history
For release 1.5.1
  • Loading branch information
saggu authored Dec 16, 2022
2 parents bd05c7d + 5ba3c87 commit a41e1e7
Show file tree
Hide file tree
Showing 16 changed files with 2,499 additions and 1,221 deletions.
586 changes: 555 additions & 31 deletions docs/transform/query.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion kgtk/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.5.0'
__version__ = '1.5.1'
2 changes: 1 addition & 1 deletion kgtk/kypher/funccore.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def translate_call_to_sql(self, query, expr, state):
prob = expr.args[1].value
return f'{self.get_name()}({arg}, {prob})'
else:
raise Exception("Illegal LIKELIHOOD expression")
raise KGTKException("Illegal LIKELIHOOD expression")

Likelihood(name='likelihood').define()

Expand Down
4 changes: 2 additions & 2 deletions kgtk/kypher/funcvec.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ def has_join_controller(self, query, clause, state):
"""
rel = clause[1]
vrel = rel.variable
for mclause in query.get_match_clauses():
for mclause in query.get_top_level_match_clauses():
for pclause in mclause.get_pattern_clauses():
prel = pclause[1]
# test if we have a join controller clause that matches on the variable:
Expand Down Expand Up @@ -796,7 +796,7 @@ def get_sim_function(query, vrel, state):
"""Find a qualifying similarity function linked to this controller via 'vrel'.
Return None if nothing could be found.
"""
for mclause in query.get_match_clauses():
for mclause in query.get_top_level_match_clauses():
for pclause in mclause.get_pattern_clauses():
prel = pclause[1]
# test variable name match, but ensure that the variable is from a different clause:
Expand Down
12 changes: 10 additions & 2 deletions kgtk/kypher/grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,15 +255,23 @@
| S I N G L E WS '(' WS FilterExpression:fex WS ')' -> ["Single", fex]
| RelationshipsPattern
| GraphRelationshipsPattern
| ExplicitExistsExpression
| ExistsFunction
| parenthesizedExpression
| FunctionInvocation
| Variable
parenthesizedExpression = '(' WS Expression:ex WS ')' -> ex
RelationshipsPattern = NodePattern:np (WS PatternElementChain)?:pec -> ["RelationshipsPattern", np, pec]
RelationshipsPattern = NodePattern:np (WS PatternElementChain)*:pec
-> ["ImplicitExistsExpression", [['PatternPart', None, ['PatternElement', np, pec]]], None]
GraphRelationshipsPattern = Variable:v ':' WS NodePattern:np (WS PatternElementChain)?:pec -> ["GraphRelationshipsPattern", v, np, pec]
GraphRelationshipsPattern = Variable:v ':' WS NodePattern:np (WS PatternElementChain)*:pec
-> ["ImplicitExistsExpression", [['GraphPatternPart', v, ['PatternElement', np, pec]]], None]
ExplicitExistsExpression = E X I S T S WS "{" WS Pattern:p (WS Where)?:w WS "}" -> ["ExplicitExistsExpression", p, w]
ExistsFunction = E X I S T S WS "(" WS Pattern:p WS ")" -> ["ExistsFunction", p, None]
FilterExpression = IdInColl:i (WS Where)?:w -> ["FilterExpression", i, w]
Expand Down
2,309 changes: 1,210 additions & 1,099 deletions kgtk/kypher/grammar_compiled.py

Large diffs are not rendered by default.

78 changes: 67 additions & 11 deletions kgtk/kypher/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
import os
import os.path
import pprint
import re
import parsley
import ometa.grammar

from kgtk.exceptions import KGTKException
from kgtk.kypher.grammar import KYPHER_GRAMMAR

pp = pprint.PrettyPrinter(indent=4)
Expand Down Expand Up @@ -137,11 +140,32 @@ def has_element(obj, test):
if has_element(elt, test):
return True
elif isinstance(obj, dict):
for elt in obj.values():
if has_element(elt, test):
for key, val in obj.items():
if key.startswith('_') or val is None:
continue
if has_element(val, test):
return True
return False

def collect_elements(obj, test, coll=None):
"""Collect any of the 'QueryElement's in `obj' and/or any of their
recursive subelements that satisfy 'test' and return them as a list.
"""
coll = [] if coll is None else coll
if isinstance(obj, QueryElement):
if test(obj):
coll.append(obj)
collect_elements(obj.__dict__, test, coll)
elif isinstance(obj, list):
for elt in obj:
collect_elements(elt, test, coll)
elif isinstance(obj, dict):
for key, val in obj.items():
if key.startswith('_') or val is None:
continue
collect_elements(val, test, coll)
return coll


### Object representation for Kypher ASTs (Abstract Syntax Trees)

Expand Down Expand Up @@ -445,6 +469,8 @@ def __init__(self, query, *args):
# We handle this here with a "change class" to `PatternElement' which is what it should be.
# TO DO: clean this up, but it will do for now:
# make this return a PathPattern to make normalization work similar to MATCH
# NOTE: since we are translating Graph/RelationshipPattern into "ImplicitExistsExpression" now,
# the second 'WHERE case' incongruent signature has been eliminated as a side-effect
head = args[0]
tail = args[1] is not None and list(args[1:]) or []
PatternElement.__init__(self, query, head, tail)
Expand All @@ -458,7 +484,7 @@ def __init__(self, query, *args):
self.right_arrow = right_arrow
# a bidirectional arrow is legal in the grammar but not legal Cypher;
if left_arrow and right_arrow:
raise Exception('Illegal bidirectional arrow: %s' % str(self.simplify().to_tree()))
raise KGTKException('Illegal bidirectional arrow: %s' % str(self.simplify().to_tree()))

def simplify(self):
self.detail = simplify_object(self.detail)
Expand Down Expand Up @@ -681,6 +707,9 @@ def normalize(self):
current_graph = normpath[0].graph or current_graph
normpath[0].graph = current_graph
self.pattern_clauses.append(normpath)
# add pclause to mclause backpointers for the benefit of the query translator:
for pclause in self.pattern_clauses:
pclause[0]._match_clause = self
return self

def get_pattern_clauses(self, default_graph=None):
Expand All @@ -693,6 +722,19 @@ def get_where_clause(self):
class OptionalMatch(StrictMatch):
ast_name = 'OptionalMatch'

class ExistsExpression(StrictMatch):
# these are not top-level clauses but do behave like a strict match
pass

class ExplicitExistsExpression(ExistsExpression):
ast_name = 'ExplicitExistsExpression'

class ImplicitExistsExpression(ExistsExpression):
ast_name = 'ImplicitExistsExpression'

class ExistsFunction(ExistsExpression):
ast_name = 'ExistsFunction'

class Where(QueryElement):
ast_name = 'Where'

Expand Down Expand Up @@ -781,12 +823,14 @@ def __init__(self, query, distinct, body):
self.body = intern_ast(query, body)

def simplify(self):
body = self.body.simplify()
self.items = body.items
self.order = body.order
self.skip = body.skip
self.limit = body.limit
delattr(self, 'body')
if hasattr(self, 'body'):
body = self.body.simplify()
self.items = body.items
self.order = body.order
self.skip = body.skip
self.limit = body.limit
delattr(self, 'body')
simplify_object(self.__dict__)
return self

class With(Return):
Expand Down Expand Up @@ -834,14 +878,14 @@ def intern_ast(query, ast):
klass = AST_NAME_TABLE.get(ast[0])
if klass is not None:
return klass(query, *ast[1:])
raise Exception('Unhandled expression type: %s' % ast)
raise KGTKException('Unhandled expression type: %s' % ast)

def intern_ast_list(query, ast_list):
if ast_list is None:
return None
elif isinstance(ast_list, list):
return [intern_ast(query, ast) for ast in ast_list]
raise Exception('Unhandled list type: %s' % ast_list)
raise KGTKException('Unhandled list type: %s' % ast_list)


### Kypher query:
Expand Down Expand Up @@ -881,6 +925,11 @@ def get_optional_match_clauses(self):
optional_clauses = self.simplify().query.match.optionals
return optional_clauses

def get_all_match_clauses(self):
"""Return all strict, optional and other pattern match clauses of this query.
"""
return collect_elements(self.query, lambda x: isinstance(x, StrictMatch))

def get_with_clause(self):
assert isinstance(self.query, SingleQuery), 'Only single-match queries are supported, no unions'
self.simplify()
Expand Down Expand Up @@ -908,6 +957,13 @@ def get_limit_clause(self):
limit = ret and ret.limit or None
return limit

ANONYMOUS_VARIABLE_REGEX = re.compile(r'^_x[0-9][0-9][0-9][0-9]$')

def is_anonymous_variable(self, name):
"""Return True if 'name' matches variables created by self.create_anonymous_variable().
"""
return len(name) == 6 and self.ANONYMOUS_VARIABLE_REGEX.match(name)

def create_anonymous_variable(self):
i = 1
while True:
Expand Down
Loading

0 comments on commit a41e1e7

Please sign in to comment.