From d1e68bd3dc8f79b74233043fd8341a0439f0d939 Mon Sep 17 00:00:00 2001 From: davidmezzetti <561939+davidmezzetti@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:11:43 -0500 Subject: [PATCH 1/3] Add path groups #26, #35 --- grandcypher/__init__.py | 27 +++++++++++++++++++++++++-- grandcypher/test_queries.py | 20 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/grandcypher/__init__.py b/grandcypher/__init__.py index df2940b..8ea3805 100644 --- a/grandcypher/__init__.py +++ b/grandcypher/__init__.py @@ -46,7 +46,9 @@ many_match_clause : (match_clause)+ -match_clause : "match"i node_match (edge_match node_match)* +match_clause : "match"i path_clause? node_match (edge_match node_match)* + +path_clause : CNAME EQUAL where_clause : "where"i compound_condition @@ -111,6 +113,7 @@ LEFT_ANGLE : "<" RIGHT_ANGLE : ">" +EQUAL : "=" MIN_HOP : INT MAX_HOP : INT TYPE : CNAME @@ -309,6 +312,7 @@ def _data_path_to_entity_name_attribute(data_path): class _GrandCypherTransformer(Transformer): def __init__(self, target_graph: nx.Graph, limit=None): self._target_graph = target_graph + self._paths = [] self._where_condition: CONDITION = None self._motif = nx.DiGraph() self._matches = None @@ -327,7 +331,7 @@ def _lookup(self, data_paths: List[str], offset_limit) -> Dict[str, List]: for data_path in data_paths: entity_name, _ = _data_path_to_entity_name_attribute(data_path) - if entity_name not in motif_nodes and entity_name not in self._return_edges: + if entity_name not in motif_nodes and entity_name not in self._return_edges and entity_name not in self._paths: raise NotImplementedError(f"Unknown entity name: {data_path}") result = {} @@ -352,6 +356,20 @@ def _lookup(self, data_paths: List[str], offset_limit) -> Dict[str, List]: for node in ret ) + elif entity_name in self._paths: + ret = [] + for mapping, _ in true_matches: + path, nodes = [], list(mapping.values()) + for x, node in enumerate(nodes): + # Edge + if x > 0: + path.append(self._target_graph.get_edge_data(nodes[x - 1], node)) + + # Node + path.append(node) + + ret.append(path) + else: mapping_u, mapping_v = self._return_edges[data_path] # We are looking for an edge mapping in the target graph: @@ -603,6 +621,8 @@ def match_clause(self, match_clause: Tuple): u, ut, js = match_clause[0] self._motif.add_node(u.value, __labels__=ut, **js) return + + match_clause = match_clause[1:] if not match_clause[0] else match_clause for start in range(0, len(match_clause) - 2, 2): ((u, ut, ujs), (g, t, d, minh, maxh), (v, vt, vjs)) = match_clause[ start : start + 3 @@ -633,6 +653,9 @@ def match_clause(self, match_clause: Tuple): self._motif.add_node(u, __labels__=ut, **ujs) self._motif.add_node(v, __labels__=vt, **vjs) + def path_clause(self, path_clause: tuple): + self._paths.append(path_clause[0]) + def where_clause(self, where_clause: tuple): self._where_condition = where_clause[0] diff --git a/grandcypher/test_queries.py b/grandcypher/test_queries.py index 55a901e..3ef002a 100644 --- a/grandcypher/test_queries.py +++ b/grandcypher/test_queries.py @@ -1157,3 +1157,23 @@ def test_nested_nots_in_statements(self): res = GrandCypher(host).run(qry) assert len(res["Instrument"]) == 1 + + +class TestPath: + def test_path(self): + host = nx.DiGraph() + host.add_node("x", name="x") + host.add_node("y", name="y") + host.add_node("z", name="z") + + host.add_edge("x", "y", foo="bar") + host.add_edge("y", "z",) + + qry = """ + MATCH P = ()-[r*2]->() + RETURN P + LIMIT 1 + """ + + res = GrandCypher(host).run(qry) + assert len(res["P"][0]) == 5 From bd6be79971d8911662a61c10327d2a708242d7a2 Mon Sep 17 00:00:00 2001 From: davidmezzetti <561939+davidmezzetti@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:13:40 -0500 Subject: [PATCH 2/3] Remove unnecessary enumerate --- grandcypher/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/grandcypher/__init__.py b/grandcypher/__init__.py index 8ea3805..b9480d4 100644 --- a/grandcypher/__init__.py +++ b/grandcypher/__init__.py @@ -478,8 +478,7 @@ def _matches_iter(self, motif): # Single match clause iterator if iterators and len(iterators) == 1: - for x, match in enumerate(iterators[0]): - yield match + yield from iterators[0] # Multi match clause, requires a cartesian join else: From f905d441ba9f3eeac3e10273b6dda99d3d88c5c6 Mon Sep 17 00:00:00 2001 From: davidmezzetti <561939+davidmezzetti@users.noreply.github.com> Date: Thu, 15 Feb 2024 13:39:40 -0500 Subject: [PATCH 3/3] Update CHANGELOG and bump version in setup.py --- CHANGELOG.md | 10 +++++++++- setup.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e254f6..bdff04d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # CHANGELOG -### **0.5.0** (Unreleased) +### **0.6.0** (February 15 2024) + +> New path group operator + +#### Features + +- Support for path group operators (#37) + +### **0.5.0** (February 13 2024) > Lots of language support for new query operators. diff --git a/setup.py b/setup.py index 7abcb57..a28708a 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="grand-cypher", - version="0.5.0", + version="0.6.0", author="Jordan Matelsky", author_email="opensource@matelsky.com", description="Query Grand graphs using Cypher",