Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
e2adb0b
Remove deprecated methods
mariusconjeaud Jun 12, 2025
961e9d0
Remove use of deprecated methods
mariusconjeaud Jun 19, 2025
11ab178
Fix for AsyncDatabase and parallel transactions #888
DenesPal Aug 26, 2025
453e2e2
Merge remote-tracking branch 'origin/master' into rc/6.0.0
mariusconjeaud Sep 24, 2025
79e503b
Merge remote-tracking branch 'origin/master' into fix/888-async-datab…
mariusconjeaud Sep 24, 2025
0d8a6ae
Sync transpiling
mariusconjeaud Sep 24, 2025
b4fc0b7
Merge remote-tracking branch 'origin/master' into fix/888-async-datab…
mariusconjeaud Sep 24, 2025
a7bda88
Merge remote-tracking branch 'origin/master' into rc/6.0.0
mariusconjeaud Sep 24, 2025
3424ab2
Merge remote-tracking branch 'origin/rc/5.5.3' into fix/888-async-dat…
mariusconjeaud Sep 24, 2025
8dfef4e
Merge remote-tracking branch 'origin/rc/5.5.3' into rc/6.0.0
mariusconjeaud Sep 24, 2025
983def8
Merge remote-tracking branch 'origin/rc/6.0.0' into fix/888-async-dat…
mariusconjeaud Sep 24, 2025
b76dad0
Revert "Merge remote-tracking branch 'origin/rc/6.0.0' into fix/888-a…
mariusconjeaud Sep 24, 2025
149043a
Merge remote-tracking branch 'origin/rc/5.5.3' into rc/6.0.0
mariusconjeaud Sep 25, 2025
0c48be8
Merge remote-tracking branch 'origin/rc/6.0.0' into fix/888-async-dat…
mariusconjeaud Sep 25, 2025
1c566db
Merge pull request #889 from DenesPal/fix/888-async-database-transact…
mariusconjeaud Sep 25, 2025
f7b2b11
Remove deprecated methods (adb internal, traverse/fetch_relations)
mariusconjeaud Sep 25, 2025
6cd2c40
Fix init import
mariusconjeaud Sep 25, 2025
0c31229
Enforce strict cardinality check ; fix disconnect_all for OneOrMore
mariusconjeaud Sep 26, 2025
2410aa6
Reset soft cardinality check config after tests
mariusconjeaud Sep 26, 2025
ad9f506
Remove python 3.8
mariusconjeaud Sep 26, 2025
cb56d8f
Update changelog
mariusconjeaud Sep 26, 2025
7f42ff5
Merge pull request #902 from neo4j-contrib/rc/6.0.0-remove-deprecated
mariusconjeaud Sep 26, 2025
d33598a
Update changelog
mariusconjeaud Sep 26, 2025
3f3be7d
EOL Python 3.9
mariusconjeaud Sep 26, 2025
7b0c6f0
Migrate type hints to 3.10 (union and optional)
mariusconjeaud Sep 26, 2025
9c70d57
Migrate type hints List and Tuple
mariusconjeaud Sep 26, 2025
8c02c3d
Use match statement
mariusconjeaud Sep 26, 2025
2e55c1e
Replace relationship directions with final Enum
mariusconjeaud Sep 26, 2025
1326a41
Merge pull request #903 from neo4j-contrib/rc/6.0.0-eol-python3.9
mariusconjeaud Sep 26, 2025
37ff342
v0 - review docs
mariusconjeaud Oct 1, 2025
a9e8bad
Merge remote-tracking branch 'origin/master' into rc/6.0.0
mariusconjeaud Oct 1, 2025
651e322
Improve documentation
mariusconjeaud Oct 2, 2025
910afe3
Merge remote-tracking branch 'origin/rc/6.0.0' into rc/6.0.0-modern-c…
mariusconjeaud Oct 2, 2025
7659fbc
Migrate to new config
mariusconjeaud Oct 2, 2025
125b4c0
Implement FulltextFilter
greengori11a Oct 5, 2025
68b022c
fix issue with vectorfiltertest when run as a whole test suite
greengori11a Oct 5, 2025
5af58c5
Add topk test
greengori11a Oct 5, 2025
90f9eb6
add test limit on new test
greengori11a Oct 5, 2025
3ed9c76
issue/909 - improve error message verbosity
billycalladine Oct 6, 2025
5f04b60
Fix circular import
mariusconjeaud Oct 10, 2025
cbdb131
Add noqa for backward compatibility items
mariusconjeaud Oct 10, 2025
b6a74b5
Ignore var case sonarcloud rule for config - backward compatibility
mariusconjeaud Oct 10, 2025
aeb071e
Update file pattern
mariusconjeaud Oct 10, 2025
31ffa4d
Merge pull request #911 from neo4j-contrib/rc/6.0.0-modern-config
mariusconjeaud Oct 10, 2025
5e46692
Merge remote-tracking branch 'origin/master' into rc/6.0.0
mariusconjeaud Oct 10, 2025
0fae35b
Fix Aura sync test issues
mariusconjeaud Oct 10, 2025
5911209
Prepare version release
mariusconjeaud Oct 10, 2025
335d5b1
Increase code coverage for new config
mariusconjeaud Oct 10, 2025
d7fadf4
Add some doc
mariusconjeaud Oct 10, 2025
c47f99b
Add driver setting test for config
mariusconjeaud Oct 10, 2025
a55674b
Increase test coverage
mariusconjeaud Oct 10, 2025
b4e3592
Increase test coverage
mariusconjeaud Oct 13, 2025
afb8fdf
Merge pull request #913 from neo4j-contrib/rc/6.0.0-more-coverage
mariusconjeaud Oct 13, 2025
81eea2d
Merge branch 'rc/6.0.0' into issue/909
mariusconjeaud Oct 13, 2025
e11537f
Merge branch 'rc/6.0.0' into new-master
mariusconjeaud Oct 13, 2025
9176a5e
Fix version number in doc
mariusconjeaud Oct 13, 2025
72c2757
Fix docs
mariusconjeaud Oct 13, 2025
d192a94
Update doc and README
mariusconjeaud Oct 13, 2025
3f40d34
Update README
mariusconjeaud Oct 13, 2025
0c5394f
Merge pull request #907 from greengori11a/new-master
mariusconjeaud Oct 13, 2025
17ab4d8
Fix test
mariusconjeaud Oct 13, 2025
2cc624b
Merge pull request #910 from billycalladine/issue/909
mariusconjeaud Oct 13, 2025
87444fb
Split core file into smaller files
mariusconjeaud Oct 13, 2025
0a01617
Fix test
mariusconjeaud Oct 13, 2025
da3fdd4
Fix import issues
mariusconjeaud Oct 13, 2025
96e292f
Fix import
mariusconjeaud Oct 13, 2025
1cacbe2
Fix imports
mariusconjeaud Oct 13, 2025
c5ed5c5
Fix code smell
mariusconjeaud Oct 13, 2025
0910625
Merge pull request #914 from neo4j-contrib/rc/6.0.0-split-core-file
mariusconjeaud Oct 14, 2025
8c4448f
Prepare for neo4j driver major bump ; fix test warnings
mariusconjeaud Oct 14, 2025
3d74f0d
Remove unused import
mariusconjeaud Oct 14, 2025
8cb2559
Fix some mypy issues
mariusconjeaud Oct 14, 2025
5839119
Merge pull request #915 from neo4j-contrib/rc/6.0.0-prepare-driver-ma…
mariusconjeaud Oct 15, 2025
acbd06b
Remove core sync file
mariusconjeaud Oct 15, 2025
311f8b0
Add deprecation warning for old config system
mariusconjeaud Oct 15, 2025
b8d3135
Fix more mypy
mariusconjeaud Oct 15, 2025
180b08b
Fix test interference
mariusconjeaud Oct 17, 2025
48d5d65
Update changelog
mariusconjeaud Oct 17, 2025
d10b7f3
Automatic class resolution for raw queries does not work if the node …
mariusconjeaud Oct 20, 2025
99e2518
Fix test
mariusconjeaud Oct 20, 2025
1328571
Fix list resolution
mariusconjeaud Oct 20, 2025
642e2d7
Fix order dependent test
mariusconjeaud Oct 20, 2025
5fe8e92
Fix order dependent tests
mariusconjeaud Oct 20, 2025
cb3e42b
Update doc and readme
mariusconjeaud Oct 21, 2025
f5b9a14
Fix doc example order by rel prop
mariusconjeaud Oct 23, 2025
59d7a18
Improve batch documentation and testing
mariusconjeaud Oct 23, 2025
bb94478
Clarify `AsyncDatabase` Instantiation and Support for Multiple Instan…
mariusconjeaud Oct 23, 2025
22c39ba
Rename confusing files and tests
mariusconjeaud Oct 23, 2025
d841eee
Merge pull request #917 from neo4j-contrib/rc/6.0.0-docs-improvements
mariusconjeaud Oct 27, 2025
cd3f46b
Add merge_by parameter
mariusconjeaud Oct 28, 2025
ebd7a16
Fix for version 4
mariusconjeaud Oct 28, 2025
a822147
Update changelog
mariusconjeaud Oct 28, 2025
b79079f
Fix for version 4
mariusconjeaud Oct 28, 2025
87a3dd3
Fix for version 4
mariusconjeaud Oct 28, 2025
d5d5bd8
Merge pull request #918 from neo4j-contrib/task/custom-merge-keys
mariusconjeaud Oct 28, 2025
07be0c9
Merge remote-tracking branch 'origin/rc/6.0.0' into 906-automatic-cla…
mariusconjeaud Oct 28, 2025
4d8b831
Merge pull request #916 from neo4j-contrib/906-automatic-class-resolu…
mariusconjeaud Nov 4, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.13", "3.12", "3.11", "3.10", "3.9"]
python-version: ["3.13", "3.12", "3.11", "3.10"]
neo4j-version: ["community", "enterprise", "5.5-enterprise", "4.4-enterprise", "4.4-community"]

steps:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ coverage_report/
.coverage*
.DS_STORE
cov.xml
test/data/model_diagram.*
5 changes: 4 additions & 1 deletion .sonarcloud.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
sonar.sources = neomodel/
sonar.tests = test/
sonar.python.version = 3.9, 3.10, 3.11, 3.12, 3.13
sonar.python.version = 3.10, 3.11, 3.12, 3.13
sonar.issue.ignore.multicriteria=e1
sonar.issue.ignore.multicriteria.e1.ruleKey=python:S100
sonar.issue.ignore.multicriteria.e1.resourceKey=**/neomodel/config.py
11 changes: 11 additions & 0 deletions Changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
Version 6.0.0 2025-xx
* Modernize config object, using a dataclass with typing, runtime and update validation rules, and environment variables support
* Fix async support of parallel transactions, using ContextVar
* 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

Version 5.5.3 2025-09
* Fix duplicated code issue in the advanced querying methods
* Remove py.typed - this was a premature change, we should write stubs for full typing support first
Expand Down
32 changes: 15 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ GitHub repo found at <https://github.com/neo4j-contrib/neomodel/>.

# Requirements

**For neomodel releases 6.x :**

- Python 3.10+
- Neo4j 2025.x.x, 5.x, 4.4 (LTS)
- Neo4j Enterprise, Community and Aura are supported

**For neomodel releases 5.x :**

- Python 3.8+
Expand All @@ -37,27 +43,19 @@ GitHub repo found at <https://github.com/neo4j-contrib/neomodel/>.
Available on
[readthedocs](http://neomodel.readthedocs.org).

# New in 5.4.0

This version adds many new features, expanding neomodel's querying capabilities. Those features were kindly contributed back by the [OpenStudyBuilder team](https://openstudybuilder.com/). A VERY special thanks to [@tonioo](https://github.com/tonioo) for the integration work.

There are too many new capabilities here, so I advise you to start by looking at the full summary example in the [Getting Started guide](https://neomodel.readthedocs.io/en/latest/getting_started.html#full-example). It will then point you to the various relevant sections.
# New in 6.0.0

We also validated support for [Python 3.13](https://docs.python.org/3/whatsnew/3.13.html).
From now on, neomodel will use **SemVer (major.minor.patch)** for versioning.

# New in 5.3.0
This version introduces a modern configuration system, using a dataclass with typing, runtime and update validation rules, and environment variables support.
See the [documentation](https://neomodel.readthedocs.io/en/latest/configuration.html) section for more details.

neomodel now supports asynchronous programming, thanks to the [Neo4j driver async API](https://neo4j.com/docs/api/python-driver/current/async_api.html). The [documentation](http://neomodel.readthedocs.org) has been updated accordingly, with an updated getting started section, and some specific documentation for the async API.
[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 in 5.3.0
### Breaking changes

- config.AUTO_INSTALL_LABELS has been removed. Please use the `neomodel_install_labels` script instead. _Note : this is because of the addition of async, but also because it might lead to uncontrolled creation of indexes/constraints. The script makes you more in control of said creation._
- The Database class has been moved into neomodel.sync_.core - and a new AsyncDatabase introduced into neomodel.async_.core
- Based on Python version [status](https://devguide.python.org/versions/),
neomodel will be dropping support for Python 3.7 in an upcoming release
(5.3 or later). _This does not mean neomodel will stop working on Python 3.7, but
it will no longer be tested against it_
- Some standalone methods have been refactored into the Database() class. Check the [documentation](http://neomodel.readthedocs.org) for a full list.
* 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

Expand All @@ -84,7 +82,7 @@ You can find some performance tests made using Locust [in this repo](https://git
Two learnings from this :

* The wrapping of the driver made by neomodel is very thin performance-wise : it does not add a lot of overhead ;
* When used in a concurrent fashion, async neomodel is faster than concurrent sync neomodel, and a lot of faster than serial queries.
* When used in a concurrent fashion, async neomodel is faster than concurrent sync neomodel, and a lot faster than serial queries.

# Contributing

Expand Down
193 changes: 171 additions & 22 deletions doc/source/batch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,48 +22,200 @@ Create multiple nodes at once in a single transaction::

create_or_update()
------------------
Atomically create or update nodes in a single operation::
Atomically create or update nodes in a single operation.
The **required** and **unique** properties are used as keys to match nodes,
all other properties being used only on the resulting write operation.
For example::

class Person(StructuredNode):
name = StringProperty(required=True)
age = IntegerProperty()

people = Person.create_or_update(
{'name': 'Tim', 'age': 83},
{'name': 'Bob', 'age': 23},
{'name': 'Jill', 'age': 34},
{'name': 'Tim', 'age': 83}, # created
{'name': 'Bob', 'age': 23}, # created
{'name': 'Jill', 'age': 34}, # created
)

more_people = Person.create_or_update(
{'name': 'Tim', 'age': 73},
{'name': 'Bob', 'age': 35},
{'name': 'Jane', 'age': 24},
{'name': 'Tim', 'age': 73}, # updated
{'name': 'Bob', 'age': 35}, # updated
{'name': 'Jane', 'age': 24}, # created
)

This is useful for ensuring data is up to date, each node is matched by its required and/or unique properties. Any
additional properties will be set on a newly created or an existing node.
Custom Merge Keys
~~~~~~~~~~~~~~~~~
By default, neomodel uses all required properties as merge keys.
However, you can specify custom merge criteria using the ``merge_by`` parameter::

class User(StructuredNode):
username = StringProperty(required=True)
email = StringProperty(required=True)
full_name = StringProperty()
age = IntegerProperty()

# Default behavior (merge by username + email)
users = User.create_or_update({
'username': 'johndoe',
'email': '[email protected]',
'age': 30
})

# Custom merge by email only
users = User.create_or_update({
'username': 'johndoe',
'email': '[email protected]',
'age': 31
}, merge_by={'keys': ['email']})

# Custom merge by username only
users = User.create_or_update({
'username': 'johndoe',
'email': '[email protected]',
'age': 32
}, merge_by={'label': 'User', 'keys': ['username']})

The ``merge_by`` parameter accepts a dictionary with:
- ``label``: The Neo4j label to match against (optional, defaults to the node's inherited labels)
- ``keys``: The property name(s) to use as the merge key(s).

This is particularly useful when you want to merge nodes based on specific properties
rather than all required properties, or when you need to merge based on properties
that are not required.

Examples of different merge key configurations::

# Single key (string)
users = User.create_or_update({
'username': 'johndoe',
'email': '[email protected]',
'age': 30
}, merge_by={'keys': ['email']})

# Multiple keys (list)
users = User.create_or_update({
'username': 'johndoe',
'email': '[email protected]',
'age': 30
}, merge_by={'label': 'User', 'keys': ['username', 'email']})

# Multiple keys with different label
users = User.create_or_update({
'username': 'johndoe',
'email': '[email protected]',
'age': 30
}, merge_by={'label': 'Person', 'keys': ['email', 'age']}) # For when your node has multiple labels


Only explicitly provided properties will be updated on the node in all other cases::
class NodeWithDefaultProp(AsyncStructuredNode):
name = StringProperty(required=True)
age = IntegerProperty(default=30)
other_prop = StringProperty()

node = await NodeWithDefaultProp.create_or_update({"name": "Tania", "age": 20})
assert node[0].name == "Tania"
assert node[0].age == 20

node = await MultiRequiredPropNode.create_or_update(
{"name": "Tania", "other_prop": "other"}
)
assert node[0].name == "Tania"
assert (
node[0].age == 20
) # Tania is still 20 even though default says she should be 30
assert (
node[0].other_prop == "other"
) # She does have a brand new other_prop, lucky her !


However, if fields used as keys have default values, those default values will be used if the property is omitted in your call.
This means that when using `UniqueIdProperty`, which is both unique and has a default value, if you do not pass it explicitly,
it will generate a new (random) value for it, and thus create a new node instead of updating an existing one::

class UniquePerson(StructuredNode):
uid = UniqueIdProperty()
name = StringProperty(required=True)

unique_person = UniquePerson.create_or_update({"name": "Tim"}) # created
unique_person = UniquePerson.create_or_update({"name": "Tim"}) # created again with a new uid

.. attention::
This has been raised as an [issue in GitHub](https://github.com/neo4j-contrib/neomodel/issues/807).
While it is not a bug in itself, it is a deviation from the expected behavior of the function, and thus may be unexpected.
Therefore, an idea would be to refactor the batch mechanism to allow users to specify which properties are used as keys to match nodes.

It is important to provide unique identifiers where known, any fields with default values that are omitted will be generated.

get_or_create()
---------------
Atomically get or create nodes in a single operation::
Atomically get or create nodes in a single operation.
For example::

people = Person.get_or_create(
{'name': 'Tim'},
{'name': 'Bob'},
{'name': 'Tim'}, # created
{'name': 'Bob'}, # created
)

people_with_jill = Person.get_or_create(
{'name': 'Tim'},
{'name': 'Bob'},
{'name': 'Jill'},
{'name': 'Tim'}, # fetched
{'name': 'Bob'}, # fetched
{'name': 'Jill'}, # created
)
# are same nodes
assert people[0] == people_with_jill[0]
assert people[1] == people_with_jill[1]

This is useful for ensuring specific nodes exist only and all required properties must be specified to ensure
uniqueness. In this example 'Tim' and 'Bob' are created on the first call, and are retrieved in the second call.
The **required** and **unique** properties are used as keys to match nodes,
all other properties being used only when a new node is created.
For example::
class Person(StructuredNode):
name = StringProperty(required=True)
age = IntegerProperty()

node = await Person.get_or_create({"name": "Tania", "age": 20})
assert node[0].name == "Tania"
assert node[0].age == 20

node = await MultiRequiredPropNode.get_or_create({"name": "Tania", "age": 30})
assert node[0].name == "Tania"
assert node[0].age == 20 # Tania was fetched and not created, age is still 20

Custom Merge Keys
~~~~~~~~~~~~~~~~~
The ``get_or_create()`` method also supports the ``merge_by`` parameter for custom merge criteria::

class User(StructuredNode):
username = StringProperty(required=True, unique_index=True)
email = StringProperty(required=True, unique_index=True)
full_name = StringProperty()
age = IntegerProperty()

# Default behavior (merge by username + email)
users = User.get_or_create({
'username': 'johndoe',
'email': '[email protected]',
'age': 30
})

# Custom merge by email only
users = User.get_or_create({
'username': 'johndoe',
'email': '[email protected]',
'age': 31
}, merge_by={'keys': ['email']})

# Custom merge by username only
users = User.get_or_create({
'username': 'johndoe',
'email': '[email protected]',
'age': 32
}, merge_by={'label': 'User', 'keys': ['username']})

The same ``merge_by`` parameter format applies to both ``create_or_update()`` and ``get_or_create()`` methods.


Additionally, get_or_create() allows the "relationship" parameter to be passed. When a relationship is specified, the
matching is done based on that relationship and not globally::
matching is done based on that relationship and not globally. The relationship becomes one of the keys to match nodes::

class Dog(StructuredNode):
name = StringProperty(required=True)
Expand All @@ -81,6 +233,3 @@ matching is done based on that relationship and not globally::

# not the same gizmo
assert bobs_gizmo[0] != tims_gizmo[0]

In case when the only required property is unique, the operation is redundant. However with simple required properties,
the relationship becomes a part of the unique identifier.
Loading