Saving
saved = await gom.save(career) # fields that aren't None; relationships addedawait gom.save(career, replace=["employment_history"]) # this list is the whole listawait gom.save(person, only=["name"]) # or except_=["embedding"]await gom.save(person, null_policy=NullPolicy.CLEAR) # a field assigned None clears its propertyawait gom.save(edited, from_=loaded) # checked against the object as it was loadedcreated = await gom.create(person) # only if absent; None when it was thereupdated = await gom.update(Person, uuid, lambda it: it.model_copy(update={"name": "Ada"}))await gom.delete(PersonCareer, uuid, cascade=CascadeType.DELETE_ORPHAN)await gom.delete_all(Person, "n.name = $name", {"name": "Ada"})
await gom.save_all(chunks) # fragments in one statement per 1,000result = await gom.patch(Session, session_id, set={"last_activity_at": now}, where="n.lastActivityAt IS NULL OR n.lastActivityAt < $now", bindings={"now": now})results = await gom.patch_all(chunks, only=["embedding"]) # named fields, where each node is there
await gom.edges.relate(node_ref(Issue, a), node_ref(Person, b), "ASSIGNED_TO")await gom.edges.unrelate(node_ref(Issue, a), node_ref(Person, b), "ASSIGNED_TO")people = await gom.edges.load_related(node_ref(Issue, a), "ASSIGNED_TO", Person)-
A save is one statement, applied whole or not at all: the root, the relationships it drops, each related node and the relationship to it.
-
A save adds and removes nothing, unless a field is named in
replace.removed_targets=DELETE_UNREFERENCEDalso deletes a removed node that nothing else refers to. -
Stamps refuse a stale save. On a class with a
NodeStampfield, a save of an object loaded before another writer changed the node raisesStaleObjectErrorand writes nothing. Use the object a save returns: it carries the new stamp.updateloads, applies your change and saves what it altered, again if the node changed meanwhile. -
On Neo4j a load can be handed a stamp newer than its data. Neo4j reads under read committed, without a snapshot, so a load that runs while another writer commits can find that writer’s stamp and the data from before its write.
updatetherefore compares each field of the root as it loaded it too, under the node’s lock, and loads again if one differs: a field’s value (a set field’s items, in any order and each once or more, and aJsonPackedfield’s as the text the node held), aPropertyBag’s keys and their values, and the labels aNodeLabelsfield reads. Asavechecks the same when you give it the object as loaded, asfrom_. Withoutfrom_it has only the stamp to go on, and in that moment it can write over the other writer’s change: hold on to what you loaded and pass it, or change the node withupdate. Where only the stamp makes the round trip, as through a web form, the stamp is all there is. Memgraph reads from a snapshot, FalkorDB runs no read beside a write, and Oracle and Postgres read a statement whole, so on them the stamp is what was read. -
from_is the object as you loaded it, when what you save is a changed copy of it. The save is then checked against the stampfrom_carries, whatever stamp the copy carries, asupdatechecks against what it loaded, and on Neo4j against each field offrom_’s root too. AJsonPackedfield is compared as a save packs it:from_holds the value, not the text the node held, so where another writer packed the same value with other spacing the save is refused, andupdate, which compares the text, is the way to change the node. Afrom_of another class, with another id, or with no stamp is refused before anything is written, and so is the object saved itself. A copy may share an unchanged root withfrom_, as a view copied to change only a relationship does; change the root through a copy (model_copy), not in place.loaded = await gom.load(Person, uuid)# ... later, from what the user changed:await gom.save(loaded.model_copy(update={"email": form.email}), from_=loaded) -
save_allruns its statements in one transaction, or in yours, so a failure leaves nothing written. It writes the fragments’ classes in the order of their names and the fragments of each in the order of their ids, so two batches over the same nodes take their locks in the same order. The objects come back in the order given. -
patchwrites named fields of one node by id, if it meets a condition, without loading it, and says whether it found the node, applied the patch and changed anything. It never makes a node. -
Edges relate and unrelate stored nodes by id, marking both ends’ stamps.
-
A cascading delete of a view follows the relationships a save of it writes, through nested views. It leaves what a read-only field loads (a
ReadOnly()field, aGraphPath, and a list of fragments read over several hops): other code writes those nodes.DELETE_ALLdeletes every node the view reaches, andDELETE_ORPHANeach one that nothing outside the view still needs. That keeps a node something outside the view refers to, at any level, with everything the view holds below it: a bookmarked message keeps its attachments, and a pinned sub-session its messages and sub-sessions. The rest goes, the session that held a kept message among it.DELETE_ORPHANisn’t available on Memgraph. A view that contains itself (parts: list["SessionTree"], with amax_depth) deletes what a load of it reads, every level and what each holds through the relationships a save writes; one that reaches below itsmax_depthis refused with aDrivineErrorbefore anything is written, saying how deep the view reads: raise themax_depthto cover the deepest level. -
Outside a transaction, a save the engine turns away because another writer held the node is run again after a pause. Inside one, it joins the transaction, and fails for its owner to decide.
-
A save that lost its connection, or whose authorization expired, is not run again, though the Neo4j driver calls that retryable: it may have been applied, and a second run would find its own stamp and report a save that was applied as refused.
The store format, stamps included, is TypeScript’s and drivine4j’s, so the three can share a store.

