Skip to content

Saving

saved = await gom.save(career) # fields that aren't None; relationships added
await gom.save(career, replace=["employment_history"]) # this list is the whole list
await gom.save(person, only=["name"]) # or except_=["embedding"]
await gom.save(person, null_policy=NullPolicy.CLEAR) # a field assigned None clears its property
await gom.save(edited, from_=loaded) # checked against the object as it was loaded
created = await gom.create(person) # only if absent; None when it was there
updated = 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,000
result = 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_UNREFERENCED also deletes a removed node that nothing else refers to.

  • Stamps refuse a stale save. On a class with a NodeStamp field, a save of an object loaded before another writer changed the node raises StaleObjectError and writes nothing. Use the object a save returns: it carries the new stamp. update loads, 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. update therefore 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 a JsonPacked field’s as the text the node held), a PropertyBag’s keys and their values, and the labels a NodeLabels field reads. A save checks the same when you give it the object as loaded, as from_. Without from_ 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 with update. 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 stamp from_ carries, whatever stamp the copy carries, as update checks against what it loaded, and on Neo4j against each field of from_’s root too. A JsonPacked field 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, and update, which compares the text, is the way to change the node. A from_ 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 with from_, 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_all runs 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.

  • patch writes 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, a GraphPath, and a list of fragments read over several hops): other code writes those nodes. DELETE_ALL deletes every node the view reaches, and DELETE_ORPHAN each 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_ORPHAN isn’t available on Memgraph. A view that contains itself (parts: list["SessionTree"], with a max_depth) deletes what a load of it reads, every level and what each holds through the relationships a save writes; one that reaches below its max_depth is refused with a DrivineError before anything is written, saying how deep the view reads: raise the max_depth to 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.