Update (WHERE/DELETE/INSERT)
The WHERE/DELETE/INSERT pattern enables targeted updates to existing data in Fluree. This is the most flexible update mechanism, allowing conditional modifications, partial updates, and complex transformations.
Basic Pattern
The WHERE/DELETE/INSERT pattern has three clauses:
- WHERE: Pattern to match existing data
- DELETE: Triples to retract (using variables from WHERE)
- INSERT: Triples to assert (using variables from WHERE)
{
"@context": {
"ex": "http://example.org/ns/",
"schema": "http://schema.org/"
},
"where": [
{ "@id": "ex:alice", "schema:age": "?oldAge" }
],
"delete": [
{ "@id": "ex:alice", "schema:age": "?oldAge" }
],
"insert": [
{ "@id": "ex:alice", "schema:age": 31 }
]
}
This:
- Finds the current age of ex:alice
- Deletes that age value
- Inserts the new age value
WHERE clause capabilities
The update transaction where clause uses the same pattern grammar as JSON-LD queries, so you can use rich patterns like OPTIONAL, UNION, FILTER, VALUES, and subqueries.
Two common forms:
- Node-map: a single object (simple triple patterns)
- Array: a sequence of node-maps plus special forms (recommended for anything beyond basic matching)
Supported special forms inside the where array:
["filter", <expr>]["bind", "?var", <expr>](may include multiple var/expr pairs)["optional", <pattern>]["union", <pattern>, <pattern>, ...]["minus", <pattern>]["exists", <pattern>]/["not-exists", <pattern>]["values", <values-clause>]["query", <subquery>](subquery can useselect,groupBy, aggregates like(max ?x), etc.)["graph", <graph-name>, <pattern>]
Expression format for filter/bind supports either:
- Data expressions like
["+", "?x", 1],["and", [">=", "?age", 18], ["=", "?status", "pending"]] - S-expressions like
"(+ ?x 1)"
Graph scoping (named graphs)
JSON-LD update supports writing into user-defined named graphs (ingested via TriG or JSON-LD @graph) and scoping the update to a named graph.
Default graph for WHERE/DELETE/INSERT
Use a top-level graph key to scope the update to a named graph as the default graph:
{
"@context": { "ex": "http://example.org/ns/", "schema": "http://schema.org/" },
"graph": "http://example.org/graphs/audit",
"where": { "@id": "ex:event1", "schema:description": "?old" },
"delete": { "@id": "ex:event1", "schema:description": "?old" },
"insert": { "@id": "ex:event1", "schema:description": "new" }
}
This is the JSON-LD UPDATE analog of SPARQL UPDATE WITH <iri>:
- WHERE patterns are evaluated against the named graph
- DELETE/INSERT templates without an explicit graph are written to that named graph
Writing templates to specific graphs
There are two ways to target graphs in insert / delete templates:
- Per-node
@graph: attach a graph IRI to a node object (overrides the transaction-levelgraph)
{
"insert": [
{ "@id": "ex:event1", "@graph": "http://example.org/graphs/audit", "schema:description": "v" }
]
}
- Template sugar: inside
insert/deletearrays, use["graph", "<graph IRI>", <pattern>]
{
"insert": [
["graph", "http://example.org/graphs/audit", { "@id": "ex:event1", "schema:description": "v" }]
]
}
Notes:
graphis a graph IRI (a string like"http://example.org/graphs/audit")- Named-graph reads are available after indexing completes (see
docs/query/datasets.md)
Dataset scoping for WHERE (from / fromNamed)
JSON-LD update reuses the same dataset keys as JSON-LD query to control where the where clause reads from:
from: scopes the default graph used forwhereevaluation (equivalent to SPARQL UPDATEUSING <iri>)fromNamed: restricts which named graphs are visible towhere["graph", ...]patterns (equivalent to SPARQL UPDATEUSING NAMED <iri>)
This is why JSON-LD update uses from rather than introducing new keywords: it matches the existing JSON-LD query language vocabulary and keeps dataset configuration consistent across read-only queries and updates.
from (WHERE default graph)
When from is present, it scopes the where clause evaluation without changing where templates write:
graph(if present) controls the default graph for DELETE/INSERT templates (SPARQL UPDATEWITH)fromcontrols the default graph(s) forwhereevaluation (SPARQL UPDATEUSING)
Notes:
fromcan be:- a string graph IRI (shorthand for
{"graph": "<iri>"}) - an object with
{"graph": "<iri>"}(or{"graph": ["<iri1>", "<iri2>"]}) - an array of graph IRIs/selectors (multiple graphs are evaluated as a merged default graph)
- a string graph IRI (shorthand for
- If your
insert/deletetemplates write into the same graph as the top-levelgraph, you can omit per-template graph selection. The top-levelgraphbecomes the default target for templates that don't specify@graph(or["graph", ...]sugar). - If you want to write to multiple graphs in one update, keep a top-level
graphas the default (optional) and use per-template["graph", ...]for the exceptions.
{
"@context": { "ex": "http://example.org/ns/", "schema": "http://schema.org/" },
"graph": "http://example.org/g2",
"from": { "graph": "http://example.org/g1" },
"where": { "@id": "ex:s", "schema:description": "?d" },
"insert": [{ "@id": "ex:s", "schema:copyFromG1": "?d" }]
}
Example: read from one graph, write to two graphs
{
"@context": { "ex": "http://example.org/ns/", "schema": "http://schema.org/" },
"graph": "http://example.org/g2",
"from": { "graph": "http://example.org/g1" },
"where": { "@id": "ex:s", "schema:description": "?d" },
"insert": [
{ "@id": "ex:s", "schema:copyFromG1": "?d" },
["graph", "http://example.org/audit", { "@id": "ex:event1", "schema:description": "copied description" }]
]
}
fromNamed (WHERE named graphs allowlist)
Use fromNamed to allow (and optionally alias) named graphs for where ["graph", ...] patterns:
Notes:
- In
whereGRAPH patterns, you can reference the graph by alias (e.g."g2") or by the graph IRI (e.g."http://example.org/g2"). Aliases are just convenience names for matching. - In
insert/deletetemplates, graph selection is a write target. You can use:- the full graph IRI (
"http://example.org/g2") - a compact IRI/term that expands via
@context(e.g."ex:g2") - the
fromNamedalias (e.g."g2") for consistency within the same update transaction
- the full graph IRI (
{
"@context": { "ex": "http://example.org/ns/" },
"fromNamed": [
{ "alias": "g2", "graph": "http://example.org/g2" }
],
"where": [
["graph", "g2", { "@id": "ex:s", "ex:p": "?o" }]
],
"insert": [["graph", "g2", { "@id": "ex:s", "ex:q": "touched" }]]
}
Same example, but with a compacted graph IRI via @context:
{
"@context": { "ex": "http://example.org/ns/" },
"fromNamed": [{ "alias": "g2", "graph": "ex:g2" }],
"where": [["graph", "g2", { "@id": "ex:s", "ex:p": "?o" }]],
"insert": [["graph", "ex:g2", { "@id": "ex:s", "ex:q": "touched" }]]
}
Same idea without an explicit alias (the fromNamed string acts as its own identifier):
{
"@context": { "ex": "http://example.org/ns/" },
"fromNamed": ["ex:g2"],
"where": [["graph", "ex:g2", { "@id": "ex:s", "ex:p": "?o" }]],
"insert": [["graph", "ex:g2", { "@id": "ex:s", "ex:q": "touched" }]]
}
Simple Property Update
Update a single property value:
curl -X POST "http://localhost:8090/v1/fluree/update?ledger=mydb:main" \
-H "Content-Type: application/json" \
-d '{
"@context": {
"ex": "http://example.org/ns/",
"schema": "http://schema.org/"
},
"where": [
{ "@id": "ex:alice", "schema:email": "?oldEmail" }
],
"delete": [
{ "@id": "ex:alice", "schema:email": "?oldEmail" }
],
"insert": [
{ "@id": "ex:alice", "schema:email": "alice.new@example.org" }
]
}'
Multiple Property Updates
Update several properties at once:
{
"where": [
{ "@id": "ex:alice", "schema:name": "?oldName" },
{ "@id": "ex:alice", "schema:email": "?oldEmail" }
],
"delete": [
{ "@id": "ex:alice", "schema:name": "?oldName" },
{ "@id": "ex:alice", "schema:email": "?oldEmail" }
],
"insert": [
{ "@id": "ex:alice", "schema:name": "Alice Johnson" },
{ "@id": "ex:alice", "schema:email": "alice.j@example.org" }
]
}
Conditional Updates
Only update if condition is met:
{
"where": [
{ "@id": "ex:alice", "schema:age": "?age" },
{ "@id": "ex:alice", "ex:status": "?status" },
["filter", ["and", [">=", "?age", 18], ["=", "?status", "pending"]]]
],
"delete": [
{ "@id": "ex:alice", "ex:status": "?status" }
],
"insert": [
{ "@id": "ex:alice", "ex:status": "approved" }
]
}
The update only happens if Alice is 18+ and status is "pending".
Pattern Matching
Find and Update
Find entities matching a pattern and update them:
{
"where": [
{ "@id": "?person", "@type": "schema:Person" },
{ "@id": "?person", "ex:status": "pending" }
],
"delete": [
{ "@id": "?person", "ex:status": "pending" }
],
"insert": [
{ "@id": "?person", "ex:status": "active" }
]
}
This updates ALL people with status="pending" to status="active".
Relationship-Based Updates
Update based on relationships:
{
"where": [
{ "@id": "?employee", "schema:worksFor": "ex:company-a" },
{ "@id": "?employee", "ex:salary": "?oldSalary" },
["bind", "?newSalary", ["*", "?oldSalary", 1.1]]
],
"delete": [
{ "@id": "?employee", "ex:salary": "?oldSalary" }
],
"insert": [
{ "@id": "?employee", "ex:salary": "?newSalary" }
]
}
Gives all company-a employees a 10% raise.
Variable Transformation
Use variables from WHERE in INSERT with transformations:
{
"where": [
{ "@id": "ex:product-123", "ex:price": "?currentPrice" },
["bind", "?newPrice", ["*", "?currentPrice", 0.9]]
],
"delete": [
{ "@id": "ex:product-123", "ex:price": "?currentPrice" }
],
"insert": [
{ "@id": "ex:product-123", "ex:price": "?newPrice" },
{ "@id": "ex:product-123", "ex:previousPrice": "?currentPrice" }
]
}
Applies 10% discount and saves previous price.
Partial Updates
Update only specific properties, leaving others unchanged:
Current State:
ex:alice schema:name "Alice"
ex:alice schema:email "alice@example.org"
ex:alice schema:age 30
ex:alice schema:telephone "+1-555-0100"
Update Only Age:
{
"where": [
{ "@id": "ex:alice", "schema:age": "?oldAge" }
],
"delete": [
{ "@id": "ex:alice", "schema:age": "?oldAge" }
],
"insert": [
{ "@id": "ex:alice", "schema:age": 31 }
]
}
Result:
ex:alice schema:name "Alice" (unchanged)
ex:alice schema:email "alice@example.org" (unchanged)
ex:alice schema:age 31 (updated)
ex:alice schema:telephone "+1-555-0100" (unchanged)
Adding Properties
Add a property without WHERE (when it might not exist):
{
"insert": [
{ "@id": "ex:alice", "schema:telephone": "+1-555-0100" }
]
}
Or conditionally add if missing:
{
"where": [
{ "@id": "ex:alice", "schema:name": "?name" },
["optional", { "@id": "ex:alice", "schema:telephone": "?existingPhone" }],
["filter", ["not", ["bound", "?existingPhone"]]]
],
"insert": [
{ "@id": "ex:alice", "schema:telephone": "+1-555-0100" }
]
}
Removing Properties
Remove a property entirely:
{
"where": [
{ "@id": "ex:alice", "schema:telephone": "?phone" }
],
"delete": [
{ "@id": "ex:alice", "schema:telephone": "?phone" }
]
}
No INSERT clause—just deletes.
Multi-Value Properties
Replace One Value
{
"where": [
{ "@id": "ex:alice", "schema:email": "alice.old@example.org" }
],
"delete": [
{ "@id": "ex:alice", "schema:email": "alice.old@example.org" }
],
"insert": [
{ "@id": "ex:alice", "schema:email": "alice.new@example.org" }
]
}
Add Value
{
"insert": [
{ "@id": "ex:alice", "schema:email": "alice.work@example.org" }
]
}
Remove One Value
{
"where": [
{ "@id": "ex:alice", "schema:email": "alice.old@example.org" }
],
"delete": [
{ "@id": "ex:alice", "schema:email": "alice.old@example.org" }
]
}
Remove All Values
{
"where": [
{ "@id": "ex:alice", "schema:email": "?email" }
],
"delete": [
{ "@id": "ex:alice", "schema:email": "?email" }
]
}
Relationship Updates
Change Relationship
{
"where": [
{ "@id": "ex:alice", "schema:worksFor": "?oldCompany" }
],
"delete": [
{ "@id": "ex:alice", "schema:worksFor": "?oldCompany" }
],
"insert": [
{ "@id": "ex:alice", "schema:worksFor": "ex:company-b" }
]
}
Add Relationship
{
"insert": [
{ "@id": "ex:alice", "schema:knows": "ex:bob" }
]
}
Remove Relationship
{
"where": [
{ "@id": "ex:alice", "schema:knows": "ex:bob" }
],
"delete": [
{ "@id": "ex:alice", "schema:knows": "ex:bob" }
]
}
Complex Updates
Cascading Updates
Update related entities:
{
"where": [
{ "@id": "ex:order-123", "ex:status": "?oldStatus" },
{ "@id": "ex:order-123", "ex:items": "?item" },
{ "@id": "?item", "ex:status": "?itemStatus" }
],
"delete": [
{ "@id": "ex:order-123", "ex:status": "?oldStatus" },
{ "@id": "?item", "ex:status": "?itemStatus" }
],
"insert": [
{ "@id": "ex:order-123", "ex:status": "shipped" },
{ "@id": "?item", "ex:status": "shipped" }
]
}
Computed Values
Calculate new values based on old:
{
"where": [
{ "@id": "ex:product-123", "ex:inventory": "?current" },
{ "@id": "ex:product-123", "ex:sold": "?sold" },
["bind", "?newInventory", ["-", "?current", "?sold"]]
],
"delete": [
{ "@id": "ex:product-123", "ex:inventory": "?current" }
],
"insert": [
{ "@id": "ex:product-123", "ex:inventory": "?newInventory" }
]
}
Error Handling
No Match
If WHERE doesn't match, nothing happens (not an error):
{
"where": [
{ "@id": "ex:nonexistent", "schema:name": "?name" }
],
"delete": [...],
"insert": [...]
}
Result: No changes, no error.
Multiple Matches
If WHERE matches multiple entities, all are updated:
{
"where": [
{ "@id": "?person", "ex:status": "pending" }
],
"delete": [
{ "@id": "?person", "ex:status": "pending" }
],
"insert": [
{ "@id": "?person", "ex:status": "approved" }
]
}
Updates ALL entities with status="pending".
Comparison: WHERE/DELETE/INSERT vs Replace Mode
| Feature | WHERE/DELETE/INSERT | Replace Mode |
|---|---|---|
| Granularity | Property-level | Entity-level |
| Other properties | Preserved | Removed |
| Conditional | Yes (with filters) | No |
| Pattern matching | Yes | No |
| Idempotent | Depends on logic | Yes |
| Use case | Partial updates | Complete replacement |
Best Practices
1. Be Specific in WHERE
Good (specific):
{
"where": [
{ "@id": "ex:alice", "schema:age": "?oldAge" }
]
}
Risky (might match many):
{
"where": [
{ "@id": "?person", "schema:age": "?age" }
]
}
2. Always Use Variables
Use variables from WHERE in DELETE:
Good:
{
"where": [{ "@id": "ex:alice", "schema:age": "?oldAge" }],
"delete": [{ "@id": "ex:alice", "schema:age": "?oldAge" }]
}
Bad (deletes all ages):
{
"where": [{ "@id": "ex:alice", "schema:age": "?oldAge" }],
"delete": [{ "@id": "ex:alice", "schema:age": "?age" }]
}
3. Test Updates
Test on development data first:
// Test update logic
const result = await transact(updateQuery);
console.log(`Updated ${result.flakes_retracted} values`);
4. Use Filters for Safety
Add filters to prevent unintended updates:
{
"where": [
"...",
["filter", ["and", [">=", "?age", 0], ["<=", "?age", 150]]]
],
"delete": [...],
"insert": [...]
}
5. Handle No Matches
Decide if no matches should be an error in your application:
const result = await transact(updateQuery);
if (result.flakes_retracted === 0) {
console.warn('Update matched no entities');
}
6. Document Complex Updates
Comment complex update logic:
// Update inventory after sale completion
// - Decrement stock by sold quantity
// - Update last-sold timestamp
// - Mark as low-stock if below threshold
const updateInventory = { ... };
Performance Considerations
Index Usage
WHERE clauses use indexes:
- Subject-based: Fast
- Predicate-based: Fast
- Pattern-based: May be slower
Batch Updates
For many updates, consider batching:
const updates = entities.map(e => createUpdateQuery(e));
for (const update of updates) {
await transact(update);
}
Related Documentation
- Conditional updates (atomic / CAS patterns) - Increment, compare-and-swap, state machines, transfers
- Insert - Adding new data
- Upsert - Replace mode
- Retractions - Removing data
- Overview - Transaction overview
- Query WHERE Clauses - WHERE pattern syntax