Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Version 6.0.0 2025-xx
* Introduces merge_by parameter for batch operations to customize merge behaviour (label and property keys)
* Enforce strict cardinality check by default
* Refactor internal code: core.py file is now split into smaller files for database, node, transaction
* Fix object resolution for maps and lists Cypher objects, even when nested. This changes the way you can access lists in your Cypher results, see documentation for more info
* Make AsyncDatabase / Database a true singleton for clarity
* Remove deprecated methods (including fetch_relations & traverse_relations, replaced with traverse ; database operations like clear_neo4j_database or change_neo4j_password have been moved to db/adb singleton internal methods)
* Housekeeping and bug fixes
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ See the [documentation](https://neomodel.readthedocs.io/en/latest/configuration.

[Semantic Indexes](https://neomodel.readthedocs.io/en/latest/semantic_indexes.html#) (Vector and Full-text) are now natively supported so you do not have to use a custom Cypher query. Special thanks to @greengori11a for this.

### Breaking changes

* List object resolution from Cypher was creating "2-depth" lists for no apparent reason. This release fixes this so that, for example "RETURN collect(node)" will return the nodes directly as a list in the result. In other words, you can extract this list at `results[0][0]` instead of `results[0][0][0]`
* See more breaking changes in the [documentation](http://neomodel.readthedocs.org)

# Installation

Install from pypi (recommended):
Expand Down
1 change: 1 addition & 0 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ To install from github::
**Breaking changes in 6.0**

- The soft cardinality check is now available for all cardinalities, and strict check is enabled by default.
- List object resolution from Cypher was creating "2-depth" lists for no apparent reason. This release fixes this so that, for example "RETURN collect(node)" will return the nodes directly as a list in the result. In other words, you can extract this list at `results[0][0]` instead of `results[0][0][0]`
- AsyncDatabase / Database are now true singletons for clarity
- Standalone methods moved into the Database() class have been removed outside of the Database() class :
- change_neo4j_password
Expand Down
8 changes: 7 additions & 1 deletion neomodel/async_/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,13 @@ def _object_resolution(self, object_to_resolve: Any) -> Any:
return AsyncNeomodelPath(object_to_resolve)

if isinstance(object_to_resolve, list):
return self._result_resolution([object_to_resolve])
return [self._object_resolution(item) for item in object_to_resolve]

if isinstance(object_to_resolve, dict):
return {
key: self._object_resolution(value)
for key, value in object_to_resolve.items()
}

return object_to_resolve

Expand Down
3 changes: 0 additions & 3 deletions neomodel/async_/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -1787,9 +1787,6 @@ async def resolve_subgraph(self) -> list:
if node.__class__ is self.source and "_" not in name:
root_node = node
continue
if isinstance(node, list) and isinstance(node[0], list):
other_nodes[name] = node[0]
continue
other_nodes[name] = node
results.append(
self._to_subgraph(root_node, other_nodes, qbuilder._ast.subgraph)
Expand Down
8 changes: 7 additions & 1 deletion neomodel/sync_/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,13 @@ def _object_resolution(self, object_to_resolve: Any) -> Any:
return NeomodelPath(object_to_resolve)

if isinstance(object_to_resolve, list):
return self._result_resolution([object_to_resolve])
return [self._object_resolution(item) for item in object_to_resolve]

if isinstance(object_to_resolve, dict):
return {
key: self._object_resolution(value)
for key, value in object_to_resolve.items()
}

return object_to_resolve

Expand Down
3 changes: 0 additions & 3 deletions neomodel/sync_/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -1781,9 +1781,6 @@ def resolve_subgraph(self) -> list:
if node.__class__ is self.source and "_" not in name:
root_node = node
continue
if isinstance(node, list) and isinstance(node[0], list):
other_nodes[name] = node[0]
continue
other_nodes[name] = node
results.append(
self._to_subgraph(root_node, other_nodes, qbuilder._ast.subgraph)
Expand Down
55 changes: 3 additions & 52 deletions test/async_/test_issue283.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
More information about the same issue at:
https://github.com/aanastasiou/neomodelInheritanceTest

The following example uses a recursive relationship for economy, but the
idea remains the same: "Instantiate the correct type of node at the end of
The following example uses a recursive relationship for economy, but the
idea remains the same: "Instantiate the correct type of node at the end of
a relationship as specified by the model"
"""

import random
from test._async_compat import mark_async_test

Expand Down Expand Up @@ -123,56 +124,6 @@ async def test_automatic_result_resolution():
assert type((await A.friends_with)[0]) is TechnicalPerson


@mark_async_test
async def test_recursive_automatic_result_resolution():
"""
Node objects are instantiated to native Python objects, both at the top
level of returned results and in the case where they are returned within
lists.
"""

# Create a few entities
A = (
await TechnicalPerson.get_or_create(
{"name": "Grumpier", "expertise": "Grumpiness"}
)
)[0]
B = (
await TechnicalPerson.get_or_create(
{"name": "Happier", "expertise": "Grumpiness"}
)
)[0]
C = (
await TechnicalPerson.get_or_create(
{"name": "Sleepier", "expertise": "Pillows"}
)
)[0]
D = (
await TechnicalPerson.get_or_create(
{"name": "Sneezier", "expertise": "Pillows"}
)
)[0]

# Retrieve mixed results, both at the top level and nested
L, _ = await adb.cypher_query(
"MATCH (a:TechnicalPerson) "
"WHERE a.expertise='Grumpiness' "
"WITH collect(a) as Alpha "
"MATCH (b:TechnicalPerson) "
"WHERE b.expertise='Pillows' "
"WITH Alpha, collect(b) as Beta "
"RETURN [Alpha, [Beta, [Beta, ['Banana', "
"Alpha]]]]",
resolve_objects=True,
)

# Assert that a Node returned deep in a nested list structure is of the
# correct type
assert type(L[0][0][0][1][0][0][0][0]) is TechnicalPerson
# Assert that primitive data types remain primitive data types
assert issubclass(type(L[0][0][0][1][0][1][0][1][0][0]), basestring)


@mark_async_test
async def test_validation_with_inheritance_from_db():
"""
Expand Down
10 changes: 5 additions & 5 deletions test/async_/test_match_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,7 @@ async def test_annotate_and_collect():
.all()
)
assert len(result) == 1
assert len(result[0][1][0]) == 3 # 3 species must be there (with 2 duplicates)
assert len(result[0][1]) == 3 # 3 species must be there (with 2 duplicates)

result = (
await Supplier.nodes.traverse(
Expand All @@ -806,7 +806,7 @@ async def test_annotate_and_collect():
.annotate(Collect("species", distinct=True))
.all()
)
assert len(result[0][1][0]) == 2 # 2 species must be there
assert len(result[0][1]) == 2 # 2 species must be there

result = (
await Supplier.nodes.traverse(
Expand All @@ -832,7 +832,7 @@ async def test_annotate_and_collect():
.annotate(all_species=Collect("species", distinct=True))
.all()
)
assert len(result[0][1][0]) == 2 # 2 species must be there
assert len(result[0][1]) == 2 # 2 species must be there

result = (
await Supplier.nodes.traverse(
Expand All @@ -850,8 +850,8 @@ async def test_annotate_and_collect():
)
.all()
)
assert len(result[0][1][0]) == 2 # 2 species must be there
assert len(result[0][2][0]) == 3 # 3 species relations must be there
assert len(result[0][1]) == 2 # 2 species must be there
assert len(result[0][2]) == 3 # 3 species relations must be there


@mark_async_test
Expand Down
Loading