Skip to content

Commit 5e29751

Browse files
committed
v0.10.0
1 parent 5340cf7 commit 5e29751

File tree

5 files changed

+104
-69
lines changed

5 files changed

+104
-69
lines changed

CHANGELOG.md

+13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# CHANGELOG
22

3+
### **0.10.0** (October 18 2024)
4+
5+
> Bugfix for searching multigraphs, and other improvements for multigraphs.
6+
7+
#### Features
8+
9+
- Aliasing (`RETURN SUM(r.value) AS myvalue`) (#46, thanks @jackboyla!)
10+
11+
#### Fixes
12+
13+
- Fix bug in searching multigraphs where unwanted edges between returned nodes were returned (#48, thanks @jackboyla!)
14+
- Unify digraph and multigraph implementations (#46, thanks @jackboyla!)
15+
316
### **0.9.0** (June 11 2024)
417

518
> Support for aggregate functions like `COUNT`, `SUM`, `MIN`, `MAX`, and `AVG`.

README.md

+30-28
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
```shell
55
pip install grand-cypher
66
# Note: You will want a version of grandiso>=2.2.0 for best performance!
7-
pip install -U 'grandiso>=2.2.0'
7+
# pip install -U 'grandiso>=2.2.0'
88
```
99

1010
GrandCypher is a partial (and growing!) implementation of the Cypher graph query language written in Python, for Python data structures.
@@ -27,7 +27,7 @@ RETURN A.club, B.club
2727
""")
2828
```
2929

30-
See [examples.md](examples.md) for more!
30+
See [examples.md](docs/examples.md) for more!
3131

3232
### Example Usage with SQL
3333

@@ -62,32 +62,34 @@ RETURN
6262

6363
# Feature Parity
6464

65-
| Feature | Support | |
66-
| ------------------------------------------ | -------------------- | --- |
67-
| Multiple `MATCH` clauses || |
68-
| `WHERE`-clause filtering on nodes || |
69-
| Anonymous `-[]-` edges || |
70-
| `LIMIT` || |
71-
| `SKIP` || |
72-
| Node/edge attributes with `{}` syntax || |
73-
| `WHERE`-clause filtering on edges || |
74-
| Named `-[]-` edges || |
75-
| Chained `()-[]->()-[]->()` edges | ✅ Thanks @khoale88! | |
76-
| Backwards `()<-[]-()` edges | ✅ Thanks @khoale88! | |
77-
| Anonymous `()` nodes | ✅ Thanks @khoale88! | |
78-
| Undirected `()-[]-()` edges | ✅ Thanks @khoale88! | |
79-
| Boolean Arithmetic (`AND`/`OR`) | ✅ Thanks @khoale88! | |
80-
| `OPTIONAL MATCH` | 🛣 | |
81-
| `(:Type)` node-labels | ✅ Thanks @khoale88! | |
82-
| `[:Type]` edge-labels | ✅ Thanks @khoale88! | |
83-
| Graph mutations (e.g. `DELETE`, `SET`,...) | 🛣 | |
84-
| `DISTINCT` | ✅ Thanks @jackboyla! | |
85-
| `ORDER BY` | ✅ Thanks @jackboyla! | |
86-
| Aggregation functions (`COUNT`, `SUM`, `MIN`, `MAX`, `AVG`) | ✅ Thanks @jackboyla! | |
87-
88-
| | | |
89-
| -------------- | -------------- | ---------------- |
90-
| ✅ = Supported | 🛣 = On Roadmap | 🔴 = Not Planned |
65+
| Feature | Support |
66+
| ----------------------------------------------------------- | --------------------- |
67+
| Multiple `MATCH` clauses ||
68+
| `WHERE`-clause filtering on nodes ||
69+
| Anonymous `-[]-` edges ||
70+
| `LIMIT` ||
71+
| `SKIP` ||
72+
| Node/edge attributes with `{}` syntax ||
73+
| `WHERE`-clause filtering on edges ||
74+
| Named `-[]-` edges ||
75+
| Chained `()-[]->()-[]->()` edges | ✅ Thanks @khoale88! |
76+
| Backwards `()<-[]-()` edges | ✅ Thanks @khoale88! |
77+
| Anonymous `()` nodes | ✅ Thanks @khoale88! |
78+
| Undirected `()-[]-()` edges | ✅ Thanks @khoale88! |
79+
| Boolean Arithmetic (`AND`/`OR`) | ✅ Thanks @khoale88! |
80+
| `(:Type)` node-labels | ✅ Thanks @khoale88! |
81+
| `[:Type]` edge-labels | ✅ Thanks @khoale88! |
82+
| `DISTINCT` | ✅ Thanks @jackboyla! |
83+
| `ORDER BY` | ✅ Thanks @jackboyla! |
84+
| Aggregation functions (`COUNT`, `SUM`, `MIN`, `MAX`, `AVG`) | ✅ Thanks @jackboyla! |
85+
| Aliasing of returned entities (`return X as Y`) | ✅ Thanks @jackboyla! |
86+
| Negated edges (`where not (a)-->(b)`) | 🥺 |
87+
| `OPTIONAL MATCH` | 🥺 |
88+
| Graph mutations (e.g. `DELETE`, `SET`,...) | 🥺 |
89+
90+
| | | | |
91+
| -------------- | -------------- | ----------------- | ---------------- |
92+
| ✅ = Supported | 🛣 = On Roadmap | 🥺 = Help Welcome | 🔴 = Not Planned |
9193

9294
## Citing
9395

examples.md docs/examples.md

+14-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
## Multigraph
32

43
```python
@@ -21,13 +20,17 @@ RETURN n.name, m.name, r.amount
2120
res = GrandCypher(host).run(qry)
2221
print(res)
2322

24-
'''
23+
```
24+
25+
```python
2526
{
26-
'n.name': ['Alice', 'Bob'],
27-
'm.name': ['Bob', 'Alice'],
28-
'r.amount': [{(0, 'paid'): 12, (1, 'friends'): None, (2, 'paid'): 40}, {(0, 'paid'): 6, (1, 'paid'): None}]
27+
"n.name": ["Alice", "Bob"],
28+
"m.name": ["Bob", "Alice"],
29+
"r.amount": [
30+
{(0, "paid"): 12, (1, "paid"): 40},
31+
{(0, "paid"): 6, (1, "paid"): None},
32+
],
2933
}
30-
'''
3134
```
3235

3336
## Aggregation Functions
@@ -51,16 +54,12 @@ RETURN n.name, m.name, SUM(r.amount)
5154
"""
5255
res = GrandCypher(host).run(qry)
5356
print(res)
57+
```
5458

55-
'''
59+
```python
5660
{
57-
'n.name': ['Alice', 'Bob'],
58-
'm.name': ['Bob', 'Alice'],
59-
'SUM(r.amount)': [{'paid': 52, 'friends': 0}, {'paid': 6}]
61+
"n.name": ["Alice", "Bob"],
62+
"m.name": ["Bob", "Alice"],
63+
"SUM(r.amount)": [{"paid": 52}, {"paid": 6}],
6064
}
61-
'''
6265
```
63-
64-
65-
66-

grandcypher/__init__.py

+46-25
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@
162162
start="start",
163163
)
164164

165-
__version__ = "0.9.0"
165+
__version__ = "0.10.0"
166166

167167

168168
_ALPHABET = string.ascii_lowercase + string.digits
@@ -314,17 +314,17 @@ def _get_edge(host: nx.DiGraph, mapping, match_path, u, v):
314314

315315
def and_(cond_a, cond_b) -> CONDITION:
316316
def inner(match: dict, host: nx.DiGraph, return_endges: list) -> bool:
317-
condition_a, where_a = cond_a(match, host, return_endges)
317+
condition_a, where_a = cond_a(match, host, return_endges)
318318
condition_b, where_b = cond_b(match, host, return_endges)
319319
where_result = [a and b for a, b in zip(where_a, where_b)]
320320
return (condition_a and condition_b), where_result
321-
321+
322322
return inner
323323

324324

325325
def or_(cond_a, cond_b):
326326
def inner(match: dict, host: nx.DiGraph, return_endges: list) -> bool:
327-
condition_a, where_a = cond_a(match, host, return_endges)
327+
condition_a, where_a = cond_a(match, host, return_endges)
328328
condition_b, where_b = cond_b(match, host, return_endges)
329329
where_result = [a or b for a, b in zip(where_a, where_b)]
330330
return (condition_a or condition_b), where_result
@@ -419,7 +419,7 @@ def _filter_edge(edge, where_results):
419419
# exclude edge(s) from multiedge that don't satisfy the where condition
420420
edge = {k: v for k, v in edge[0].items() if where_results[k] is True}
421421
return [edge]
422-
422+
423423
if not data_paths:
424424
return {}
425425

@@ -480,8 +480,13 @@ def _filter_edge(edge, where_results):
480480
ret = (
481481
_filter_edge(
482482
_get_edge(
483-
self._target_graph, mapping[0], match_path, mapping_u, mapping_v
484-
), mapping[1]
483+
self._target_graph,
484+
mapping[0],
485+
match_path,
486+
mapping_u,
487+
mapping_v,
488+
),
489+
mapping[1],
485490
)
486491
for mapping, match_path in true_matches
487492
)
@@ -506,7 +511,11 @@ def _filter_edge(edge, where_results):
506511
for r in ret:
507512

508513
r = {
509-
k: v for k, v in r.items() if v.get("__labels__", None).intersection(motif_edge_labels)
514+
k: v
515+
for k, v in r.items()
516+
if v.get("__labels__", None).intersection(
517+
motif_edge_labels
518+
)
510519
}
511520
if len(r) > 0:
512521
filtered_ret.append(r)
@@ -542,7 +551,9 @@ def return_clause(self, clause):
542551
if isinstance(item, Tree) and item.data == "aggregation_function":
543552
func, entity = self._parse_aggregation_token(item)
544553
if alias:
545-
self._entity2alias[self._format_aggregation_key(func, entity)] = alias
554+
self._entity2alias[
555+
self._format_aggregation_key(func, entity)
556+
] = alias
546557
self._aggregation_attributes.add(entity)
547558
self._aggregate_functions.append((func, entity))
548559
else:
@@ -556,26 +567,26 @@ def return_clause(self, clause):
556567
self._alias2entity.update({v: k for k, v in self._entity2alias.items()})
557568

558569
def _extract_alias(self, item: Tree):
559-
'''
570+
"""
560571
Extract the alias from the return item (if it exists)
561-
'''
572+
"""
562573

563574
if len(item.children) == 1:
564575
return None
565576
item_keys = [it.data if isinstance(it, Tree) else None for it in item.children]
566-
if any(k == 'alias' for k in item_keys):
567-
# get the index of the alias
568-
alias_index = item_keys.index('alias')
577+
if any(k == "alias" for k in item_keys):
578+
# get the index of the alias
579+
alias_index = item_keys.index("alias")
569580
return str(item.children[alias_index].children[0].value)
570-
581+
571582
return None
572-
583+
573584
def _parse_aggregation_token(self, item: Tree):
574-
'''
585+
"""
575586
Parse the aggregation function token and return the function and entity
576587
input: Tree('aggregation_function', [Token('AGGREGATE_FUNC', 'SUM'), Token('CNAME', 'r'), Tree('attribute_id', [Token('CNAME', 'value')])])
577588
output: ('SUM', 'r.value')
578-
'''
589+
"""
579590
func = str(item.children[0].value) # AGGREGATE_FUNC
580591
entity = str(item.children[1].value)
581592
if len(item.children) > 2:
@@ -589,12 +600,17 @@ def _format_aggregation_key(self, func, entity):
589600
def order_clause(self, order_clause):
590601
self._order_by = []
591602
for item in order_clause[0].children:
592-
if isinstance(item.children[0], Tree) and item.children[0].data == "aggregation_function":
603+
if (
604+
isinstance(item.children[0], Tree)
605+
and item.children[0].data == "aggregation_function"
606+
):
593607
func, entity = self._parse_aggregation_token(item.children[0])
594608
field = self._format_aggregation_key(func, entity)
595609
self._order_by_attributes.add(entity)
596610
else:
597-
field = str(item.children[0]) # assuming the field name is the first child
611+
field = str(
612+
item.children[0]
613+
) # assuming the field name is the first child
598614
self._order_by_attributes.add(field)
599615

600616
# Default to 'ASC' if not specified
@@ -687,8 +703,12 @@ def _collate_data(data, unique_labels, func):
687703

688704
def returns(self, ignore_limit=False):
689705

690-
data_paths = self._return_requests + list(self._order_by_attributes) + list(self._aggregation_attributes)
691-
# aliases should already be requested in their original form, so we will remove them for lookup
706+
data_paths = (
707+
self._return_requests
708+
+ list(self._order_by_attributes)
709+
+ list(self._aggregation_attributes)
710+
)
711+
# aliases should already be requested in their original form, so we will remove them for lookup
692712
data_paths = [d for d in data_paths if d not in self._alias2entity]
693713
results = self._lookup(
694714
data_paths,
@@ -739,10 +759,10 @@ def _apply_order_by(self, results):
739759
indices = range(
740760
len(next(iter(results.values())))
741761
) # Safe because all lists are assumed to be of the same length
742-
for (sort_list, field, direction) in reversed(
762+
for sort_list, field, direction in reversed(
743763
sort_lists
744764
): # reverse to ensure the first sort key is primary
745-
765+
746766
if all(isinstance(item, dict) for item in sort_list):
747767
# (for edge attributes) If all items in sort_list are dictionaries
748768
# example: ([{(0, 'paid'): 9, (1, 'paid'): 40}, {(0, 'paid'): 14}], 'DESC')
@@ -761,7 +781,8 @@ def _apply_order_by(self, results):
761781
# then sort the indices based on the sorted sublists
762782
indices = sorted(
763783
indices,
764-
key=lambda i: list(sort_list[i].values())[0] or 0, # 0 if `None`
784+
key=lambda i: list(sort_list[i].values())[0]
785+
or 0, # 0 if `None`
765786
reverse=(direction == "DESC"),
766787
)
767788
# update results with sorted edge attributes list

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="grand-cypher",
8-
version="0.9.0",
8+
version="0.10.0",
99
author="Jordan Matelsky",
1010
author_email="[email protected]",
1111
description="Query Grand graphs using Cypher",

0 commit comments

Comments
 (0)