field=, field_=, and the magic this + โฆ.
Updating a Zef graph isn't imperative. You don't "read, modify, write." You declare what a node should look like, and the engine computes the diff.
An update is a list of entity declarations. Each declaration includes an identity (UID or index) that tells Zef which node to update.
No specific mutation API โ you just describe what you want.
# Replace whatever name is on this Person with 'Bob'
ET.Person('๐-abc...', name='Bob')
Any existing name edge gets removed; a new one pointing to "Bob" is created. Enforces cardinality of 1.
ET.Person('๐-abc...', likes_={'๐', '๐บ'})
The person's entire likes set is now exactly {'๐', '๐บ'}. Any previous likes not in the new set are removed.
this + trick)ET.Person('๐-abc...', likes_=this + {'๐ง'}) # add
ET.Person('๐-abc...', likes_=this - {'๐'}) # remove
The this symbol stands for "the current value of this field." But it's not actually read at write time โ it's a symbolic instruction that the engine turns into a diff.
Because reading then writing has a race condition window. When you say
likes_=this + {'๐ง'}, you're handing Zef an intent ("append"),
not a value ("replace with this new list"). The engine can apply it
atomically at the right moment.
| you write | meaning | before | after |
|---|---|---|---|
name='Bob' | set single | name='Alice' | name='Bob' |
likes_={'a','b'} | replace set | likes={'x','y'} | likes={'a','b'} |
likes_=this + {'c'} | append | likes={'a','b'} | likes={'a','b','c'} |
likes_=this - {'a'} | remove | likes={'a','b','c'} | likes={'b','c'} |
likes_={} | clear | likes={'a','b'} | likes={} |
Updates live inside a Graph([...]) declaration, just like creations. Zef figures out from the identity whether something exists already:
# Initial state
g1 = Graph([
ET.Person('๐-abc...', name='Alice', likes_={'โ', '๐ฟ'}),
])
g1.add_to_graph_store()
# Update โ add ๐บ, remove ๐ฟ, rename Alice to Ali
g2 = Graph([
ET.Person('๐-abc...',
name='Ali',
likes_=(this + {'๐บ'}) - {'๐ฟ'},
),
])
g2.add_to_graph_store()
The same syntax creates or updates depending on identity:
ET.Person(name='Bob') # CREATE โ no identity given
ET.Person('๐-abc...', name='Bob') # UPDATE if exists, CREATE if not
ET.Person(7, name='Bob') # UPDATE the 7th Person node
Nesting works the same in updates as in creates. Each declaration stands on its own:
Graph([
ET.Person('๐-abc...',
lives_in=ET.City('๐-xyz...', population=4_000_000),
),
])
# Interpreted as:
# - update Person(๐-abc) to point 'lives_in' at City(๐-xyz)
# - update City(๐-xyz) to have population=4000000
# - if either node doesn't exist, create it
Handy for scripting:
# Assume the graph already has 3 Cats
Graph([
ET.Cat(1, name='Whiskers'), # updates the 1st Cat
ET.Cat(2, name='Fluffy'), # updates the 2nd Cat
ET.Cat(4, name='Felix'), # creates the 4th Cat
])
Ordinals are great for fixtures, tests, and script loads โ deterministic and typo-proof. But they're graph-local โ don't use them to refer to "the same logical entity" across different graphs. For cross-graph stability, use UIDs.
To delete, clear the field to {} or tombstone the entire entity:
# Clear a field
ET.Person('๐-abc...', likes_={})
# Tombstone the entity
ET.Person('๐-abc...', _deleted=True) # (syntax may evolve)
Graph([
ET.Person('๐-abc...', name='Alice'),
ET.Person('๐-abc...', name='Bob'), # same UID, different name!
])
# โ Error: conflicting value for single field 'name'
The "unification" stage notices that you've asserted two different values for a single-valued field on the same entity and errors out.
# DAY 1 โ create
Graph([
ET.Post('๐-p1',
title='hello world',
author=ET.User('๐-u1', name='Alice'),
tag_={'intro'},
),
]).add_to_graph_store()
# DAY 2 โ edit title and add a tag
Graph([
ET.Post('๐-p1',
title='Hello, World! (v2)',
tag_=this + {'zef'},
),
]).add_to_graph_store()
# DAY 3 โ co-author added
Graph([
ET.Post('๐-p1',
co_author=ET.User('๐-u2', name='Bob'), # a brand-new field!
),
]).add_to_graph_store()
Notice: the co_author field appears for the first time on day 3.
No schema migration. The graph shape just evolved.
Updates are declarations, not commands. You write what the node should
be, Zef makes it so. Differential updates with this +/- let you
append/remove without racy reads. Your entity model can grow organically
without a single migration.
Start with an Alice who likes coffee. Write three updates:
UID = '๐-ali0000000000000alice'
# initial
Graph([ET.Person(UID, name='Alice', likes_={'โ'})]).add_to_graph_store()
# 1. add tea
Graph([ET.Person(UID, likes_=this + {'๐ต'})]).add_to_graph_store()
# 2. set role
Graph([ET.Person(UID, role='admin')]).add_to_graph_store()
# 3. clear likes
Graph([ET.Person(UID, likes_={})]).add_to_graph_store()
Next up: crawl โ the clean way to turn a graph into a tree. โ