Upsert
Upsert operations replace the values of the predicates you supply for an entity (matched by @id). Repeating an upsert is idempotent (see Idempotency).
What is Upsert?
Upsert = Update or Insert:
- If the entity exists: for each predicate present in your payload, retract existing values for that predicate and assert the new value(s)
- If the entity doesn’t exist: create it with the supplied triples
This makes upserts safe to retry: sending the same upsert repeatedly produces the same current-state values for those predicates.
HTTP Endpoint
Use the dedicated upsert endpoint:
curl -X POST "http://localhost:8090/v1/fluree/upsert?ledger=mydb:main" \
-H "Content-Type: application/json" \
-d '{
"@context": {
"ex": "http://example.org/ns/",
"schema": "http://schema.org/"
},
"@graph": [
{
"@id": "ex:alice",
"@type": "schema:Person",
"schema:name": "Alice Smith",
"schema:email": "alice.smith@example.org"
}
]
}'
Upsert Behavior
First Transaction (Entity Doesn’t Exist)
{
"@graph": [
{
"@id": "ex:alice",
"@type": "schema:Person",
"schema:name": "Alice",
"schema:email": "alice@example.org"
}
]
}
Result: Entity created with specified properties.
Triples After t=1:
ex:alice rdf:type schema:Person
ex:alice schema:name "Alice"
ex:alice schema:email "alice@example.org"
Second Transaction (Entity Exists)
{
"@graph": [
{
"@id": "ex:alice",
"@type": "schema:Person",
"schema:name": "Alice Smith",
"schema:email": "alice.smith@example.org",
"schema:age": 30
}
]
}
Operations:
- For each predicate in the payload (
rdf:type,schema:name,schema:email,schema:age), retract every value ex:alice currently holds for it - Assert the payload’s values
A value the payload restates cancels its own retraction, so it does not appear in the commit.
Flakes:
# Retractions (t=2)
ex:alice schema:name "Alice" (retract)
ex:alice schema:email "alice@example.org" (retract)
# Assertions (t=2)
ex:alice schema:name "Alice Smith" (assert)
ex:alice schema:email "alice.smith@example.org" (assert)
ex:alice schema:age 30 (assert)
Triples After t=2:
ex:alice rdf:type schema:Person
ex:alice schema:name "Alice Smith"
ex:alice schema:email "alice.smith@example.org"
ex:alice schema:age 30
rdf:type schema:Person is unchanged: it is retracted and re-asserted in the
same transaction, and the two cancel.
Language Tags, Lists, and Blank Nodes
The unit of replacement is the whole (graph, subject, predicate): every
current value is retracted exactly as it is stored.
- Language tags. All languages of a predicate are replaced together. An
upsert of
{"schema:name": {"@value": "Alicia", "@language": "es"}}over"Alice"@enand"Alice"@frleaves only"Alicia"@es. To change one language and keep the others, include every language in the payload, or replace that language with an update (Replacing One Language). - Lists. A
@listvalue is replaced as a whole: every stored position is retracted, including repeated values, and positions the new list restates cancel. Replacing a list with plain values (or the reverse) works the same way. - Blank nodes. A blank node in an upsert payload is identified by the
payload itself, so upserting the same document again addresses the same
node, and its values are replaced like any other subject’s. An identical
re-upsert of a document with blank nodes commits nothing. A changed
document mints new blank nodes; the old ones stay (see
Stable blank-node ids
for editing a stored blank node in place). For Turtle and TriG the
identity comes from the parsed statements as they are stored: their order
does not count, nor how a term is spelled (a prefixed or full IRI,
1or"1"^^xsd:integer,trueor"true"^^xsd:boolean, a language tag in any case), and a statement written twice counts once. Blank-node labels do count: renaming_:bto_:cgives new nodes. See How RDF text is read for the details and a query that lists blank nodes nothing references. - Named graphs. The graph is part of the unit: an upsert into a named graph replaces that graph’s values and leaves the same predicate’s values in other graphs alone.
Replacing One Language
A client that writes one language at a time, such as one prefLabel per
language, cannot use upsert for it: each upsert replaces every language of
the predicate. It replaces the one language with an update instead, which
deletes the stored values in that language and inserts the new one. Over
"Alice"@en and "Alice"@fr, this leaves "Alice"@en and "Alicia"@fr:
PREFIX schema: <http://schema.org/>
PREFIX ex: <http://example.org/ns/>
DELETE { ex:alice schema:name ?o }
INSERT { ex:alice schema:name "Alicia"@fr }
WHERE { OPTIONAL { ex:alice schema:name ?o FILTER(lang(?o) = "fr") } }
The OPTIONAL keeps the insert when no French name is stored yet.
Idempotency
Replace mode is idempotent—repeated submissions produce the same result:
First Submission (t=1):
{"@id": "ex:alice", "schema:name": "Alice", "schema:age": 30}
Result: Entity created.
Second Submission:
{"@id": "ex:alice", "schema:name": "Alice", "schema:age": 30}
Result: nothing is committed. Each value’s retraction and re-assertion cancel, so the transaction stages no flakes and the ledger stays at t=1. This holds for language-tagged values, lists, and the payload’s blank nodes too.
This makes upserts safe to retry, including for a value stored with less
detail than it was written: an integer too large for 64 bits written with an
XSD subtype such as xsd:nonNegativeInteger (the index keeps no subtype). The
upsert retracts such a value as stored and asserts it as written, and storage
holds the two as one fact, so they cancel as an unchanged value does. Graph
sync behaves the same way. (A dateTime or time keeps six fractional digits
from the moment it is written; see Datatypes.)
Comparison: Insert vs Update vs Upsert
Insert
POST /insert?ledger=mydb:main
Behavior:
- Additive: asserts the triples you submit
- Does not retract existing values automatically
Example:
t=1: INSERT { ex:alice schema:name "Alice", schema:age 30 }
t=2: INSERT { ex:alice schema:email "alice@example.org" }
Result: ex:alice has name, age, AND email (all three)
Update (WHERE/DELETE/INSERT)
POST /update?ledger=mydb:main
Behavior:
- Explicit: you retract exactly what you match in
where/delete, then assertinsert - Most flexible (conditional updates, partial updates, computed values)
Example:
t=1: INSERT { ex:alice schema:name "Alice", schema:age 30 }
t=2: UPDATE { DELETE { ex:alice schema:age 30 } INSERT { ex:alice schema:age 31 } WHERE { ex:alice schema:age 30 } }
Result: ex:alice has name "Alice", age 31
Upsert
POST /upsert?ledger=mydb:main
Behavior:
- Replaces values for the predicates you supply (per subject)
- Leaves other predicates unchanged
- Retry-safe/idempotent for the supplied predicates
Use Cases
1. Synchronization from External Systems
Sync data from external database:
async function syncUser(externalUser) {
await fetch('http://localhost:8090/v1/fluree/upsert?ledger=mydb:main', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"@graph": [{
"@id": `ex:user-${externalUser.id}`,
"@type": "schema:Person",
"schema:name": externalUser.name,
"schema:email": externalUser.email,
"schema:telephone": externalUser.phone
}]
})
})
}
// Safe to call repeatedly—always matches external state
await syncUser(fetchUserFromDB(123));
2. Idempotent API Operations
Make API operations retry-safe:
// Safe to retry on failure
async function updateProduct(productId, productData) {
return await fetch('http://localhost:8090/v1/fluree/upsert?ledger=mydb:main', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"@graph": [{
"@id": `ex:product-${productId}`,
...productData
}]
})
})
}
3. Configuration Management
Update configuration atomically:
{
"@graph": [
{
"@id": "ex:config",
"@type": "ex:Configuration",
"ex:apiEndpoint": "https://api.example.com",
"ex:timeout": 30000,
"ex:retries": 3,
"ex:enabled": true
}
]
}
Each update replaces entire configuration—no orphaned settings.
4. State Machine Transitions
Model state machines where entity has well-defined state:
{
"@graph": [
{
"@id": "ex:order-123",
"@type": "ex:Order",
"ex:status": "shipped",
"ex:shippedAt": "2024-01-22T10:30:00Z",
"ex:carrier": "FedEx",
"ex:trackingNumber": "123456789"
}
]
}
Batch Upserts
Upsert multiple entities:
POST /upsert?ledger=mydb:main
{
"@context": {
"ex": "http://example.org/ns/",
"schema": "http://schema.org/"
},
"@graph": [
{
"@id": "ex:user-1",
"@type": "schema:Person",
"schema:name": "Alice"
},
{
"@id": "ex:user-2",
"@type": "schema:Person",
"schema:name": "Bob"
},
{
"@id": "ex:user-3",
"@type": "schema:Person",
"schema:name": "Carol"
}
]
}
Each entity is replaced independently.
Type Handling
Types are Preserved
Upsert preserves existing @type values unless you explicitly include @type in the upsert payload (in which case rdf:type is treated like any other predicate and its values are replaced for that subject).
{
"@graph": [
{
"@id": "ex:alice",
"@type": "schema:Person",
"schema:name": "Alice"
}
]
}
The @type is always asserted, even if it existed before.
Multiple Types
Entities can have multiple types:
{
"@graph": [
{
"@id": "ex:alice",
"@type": ["schema:Person", "ex:Employee"],
"schema:name": "Alice"
}
]
}
All types are replaced together.
Edge Cases
Predicates Not in the Payload
Only the predicates the payload names are replaced:
Before (t=1):
{
"@id": "ex:alice",
"schema:name": "Alice",
"schema:email": "alice@example.org",
"schema:age": 30
}
Upsert (t=2):
{
"@id": "ex:alice",
"schema:name": "Alice Smith"
}
After t=2:
{
"@id": "ex:alice",
"schema:name": "Alice Smith",
"schema:email": "alice@example.org",
"schema:age": 30
}
To remove a predicate’s values, retract them with WHERE/DELETE/INSERT, or use graph sync to make a whole graph equal a document.
Error Handling
Same Errors as Default Mode
Replace mode has same validation errors:
{
"error": "ValidationError",
"message": "Invalid IRI format",
"code": "INVALID_IRI"
}
No Special Errors
Replace mode doesn’t introduce new error types—it’s just different semantics for the same operations.
Performance Considerations
Retraction Overhead
Replace mode may retract many triples:
Entity with 50 properties:
- 50 retractions
- 50 assertions
= 100 flakes per entity
For entities with many properties, this can be expensive.
Indexing Impact
Each retraction and assertion updates indexes:
- More work for indexing process
- May increase indexing lag
- Consider batch size for large replacements
Best Practices
1. Use for Idempotent Operations
Good use:
// Idempotent sync
await upsertUser(userId, userData);
await upsertUser(userId, userData); // Safe to retry
2. Include All Required Properties
Always include all properties entity should have:
Good:
{
"@id": "ex:user-123",
"@type": "schema:Person",
"schema:name": "Alice",
"schema:email": "alice@example.org",
"ex:status": "active"
}
Bad (incomplete):
{
"@id": "ex:user-123",
"schema:name": "Alice"
}
3. Use Consistent Schema
Define entity schema and always include all fields:
function createUserTransaction(user) {
return {
"@id": `ex:user-${user.id}`,
"@type": "schema:Person",
"schema:name": user.name || null,
"schema:email": user.email || null,
"schema:telephone": user.phone || null,
"ex:status": user.status || "active"
};
}
4. Document Upsert Usage
Comment when using upsert for idempotent sync:
// Upsert for idempotent sync with external API
await fetch('http://localhost:8090/v1/fluree/upsert?ledger=users:main', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userPayload),
});
5. Test Idempotency
Verify operations are truly idempotent:
const result1 = await upsert(data);
const result2 = await upsert(data);
// Should produce same final state
6. Monitor Performance
Track metrics for replace operations:
- Flakes retracted
- Flakes asserted
- Commit time
- Indexing lag
7. Consider Alternatives
For partial updates, use WHERE/DELETE/INSERT:
{
"where": [{ "@id": "ex:alice", "schema:age": "?oldAge" }],
"delete": [{ "@id": "ex:alice", "schema:age": "?oldAge" }],
"insert": [{ "@id": "ex:alice", "schema:age": 31 }]
}
Comparison Table
| Feature | Default Mode | Replace Mode |
|---|---|---|
| Behavior | Additive | Replaces each named predicate’s values |
| Predicates not in the payload | Preserved | Preserved |
| Idempotent | No | Yes, with one exception (see Idempotency) |
| Partial updates | Yes (with WHERE/DELETE/INSERT) | Per predicate |
| Use case | Adding data | Synchronization |
| Retry safety | Requires care | Safe by default, with the same exception |
| Performance | Fewer operations | More operations |
Related Documentation
- Insert - Adding new data
- Update - Partial updates
- Overview - Transaction overview
- API Endpoints - HTTP API details