Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. WHERE: Pattern to match existing data
  2. DELETE: Triples to retract (using variables from WHERE)
  3. 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:

  1. Finds the current age of ex:alice
  2. Deletes that age value
  3. Inserts the new age value

A DELETE retracts only facts that are stored. A template instantiated with a value the ledger does not hold (a constant that is absent, or a value bound by a different pattern) retracts nothing, and cannot cancel an INSERT of the same triple: DELETE { x } INSERT { x } WHERE {} inserts x when it is absent. See Retraction Semantics.

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 use select, 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 (unless from is also given)
  • DELETE/INSERT templates without an explicit graph are written to that named graph
  • A graph that doesn’t exist reads as empty, so the WHERE matches nothing; it never falls back to the ledger’s default 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-level graph)
{
  "insert": [
    { "@id": "ex:event1", "@graph": "http://example.org/graphs/audit", "schema:description": "v" }
  ]
}
  • Template sugar: inside insert / delete arrays, use ["graph", "<graph IRI>", <pattern>]
{
  "insert": [
    ["graph", "http://example.org/graphs/audit", { "@id": "ex:event1", "schema:description": "v" }]
  ]
}

Notes:

  • graph is a graph IRI (a string like "http://example.org/graphs/audit"), a compact IRI your @context expands, or a fromNamed alias. Relative graph names are refused. Two keywords name built-in graphs: "default" (the default graph) and "config" (the ledger’s config graph).
  • The ledger’s own address (urn:fluree:mydb:main, mydb:main or mydb, with no #fragment and no time pin) names the ledger’s default graph in every graph position: a top-level graph, from, fromNamed, a per-node @graph and a ["graph", …] template. The update reads and writes the default graph and never creates a named graph called by the address, and mydb:main#<graph IRI> names that graph. A graph or @graph spelled as the address is accepted even when the @context defines no mydb prefix, as from is. See the ledger’s own address in a graph position. urn:fluree:mydb:main#config is the config graph, not the address.
  • urn:default names the default graph in a top-level graph, in from and fromNamed, and in a where graph pattern (["graph", "urn:default", …], or a node’s "@graph": "urn:default"). In a template, a node’s @graph or a ["graph", …] naming urn:default is refused with a 400, since the default graph is not a named graph. A fromNamed alias spelled urn:default that names another graph is refused with a 400 too: the name already means the default graph (listing urn:default itself is fine).
  • A node’s graph covers the nodes nested in it: every template a node and its nested nodes produce goes to the node’s @graph, or the enclosing scope’s graph when it has none. A nested node’s own @graph wins for its subtree, so "@graph": "default" on a nested node sends that subtree to the default graph.
  • A graph name can be a WHERE variable, as with SPARQL GRAPH ?g: "@graph": "?g" (or ["graph", "?g", ...]) writes into the graph ?g is bound to for each solution. Variables are accepted only in update templates.
  • A node-level @graph in where (on a top-level or a nested node) resolves its name exactly as in insert and delete: a compact IRI expands against the @context, a fromNamed alias matches its graph, "config" and "txn-meta" name this ledger’s graphs, and "default" names the ledger’s default graph, as "urn:default" does, also when a top-level graph or from gives the where another default graph (an update’s where never reads a union default graph). So {"where": {"@id": "ex:a", "@graph": "ex:g", "ex:p": "?v"}, "delete": {"@id": "ex:a", "@graph": "ex:g", "ex:p": "?v"}} deletes what an insert with the same "@graph": "ex:g" wrote, and {"graph": "ex:g", "where": {"@id": "?s", "@graph": "default", "ex:p": "?o"}, "delete": {"@id": "?s", "@graph": "default", "ex:p": "?o"}, "insert": {"@id": "?s", "ex:p": "?o"}} moves ex:p from the default graph into ex:g. A where pattern cannot leave the graph an enclosing selector chose, so "default" inside one is refused. A ["query", …] subquery in where names graphs as the where around it does: the same aliases and keywords, and the same refusal inside an enclosing graph. A request’s opts.unionDefaultGraph is refused on an update with a 400, as a SPARQL update refuses the union-default-graph pragma.
  • Edge annotations (@annotation) inside a template follow their edge into its graph.
  • Named-graph reads are available after indexing completes (see docs/query/datasets.md)
{
  "@context": { "ex": "http://example.org/ns/" },
  "where":  [["graph", "?g", { "@id": "ex:event1", "ex:status": "?old" }]],
  "delete": [["graph", "?g", { "@id": "ex:event1", "ex:status": "?old" }]],
  "insert": [["graph", "?g", { "@id": "ex:event1", "ex:status": "closed" }]]
}

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 for where evaluation (equivalent to SPARQL UPDATE USING <iri>)
  • fromNamed: restricts which named graphs are visible to where ["graph", ...] patterns (equivalent to SPARQL UPDATE USING NAMED <iri>)

Their values resolve in the ledger being updated, as SPARQL USING does: the ledger’s own address ("mydb:main", "urn:fluree:mydb:main") names its default graph, "mydb:main#config" the config graph, and a registered graph IRI (a prefix the @context defines expands) that graph. A graph the ledger does not have contributes nothing, so the where binds nothing, including an IRI that also reads as another ledger’s address ("urn:g1", "other:main"). An address that can only name another ledger ("other"), or an address with a time pin, is refused with a 400.

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 UPDATE WITH)
  • from controls the default graph(s) for where evaluation (SPARQL UPDATE USING)

Notes:

  • from can 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); "default" (or "urn:default") in the array adds the default graph to the merge
  • A from graph that doesn’t exist contributes nothing, so a lone unknown from graph gives the where an empty default graph.
  • If your insert / delete templates write into the same graph as the top-level graph, you can omit per-template graph selection. The top-level graph becomes 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 graph as 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 where ["graph", …] patterns, you can reference the graph by alias (e.g. "g2"), by the graph IRI (e.g. "http://example.org/g2"), or by a compact IRI your @context expands. A name a fromNamed entry spells (its alias, or its graph as listed) is taken as written, even when it reads as a compact IRI. Aliases are just convenience names for matching. A node-level @graph in where also takes compact IRIs and the keywords, as templates do (see the notes above).
  • In insert / delete templates, 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 fromNamed alias (e.g. "g2") for consistency within the same update transaction
  • Each name in fromNamed (a graph’s IRI, its alias, and the name it is listed by) must mean one graph across the list, as a query’s dataset aliases must: a list that gives one name to two graphs is refused with a 400, since the where and the templates would read it differently. Listing one graph twice is fine.
{
  "@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" }
  ]
}

Editing Blank-Node Structures (Stable _:fdb- Ids)

Fluree skolemizes every blank node at insert time into the reserved _:fdb-... label space, and queries return those labels as the node’s @id. These ids are stable: referencing an _:fdb-... id in a later query or transaction denotes the existing stored node rather than minting a fresh one (the blank-node-syntax equivalent of RDF 1.1 §3.5 skolem IRIs). This makes blank-node-rooted structures — OWL restrictions, address objects, RDF lists — editable in place, without retracting and re-asserting the whole subtree.

Workflow: query for the node’s id, then use it as an ordinary @id:

{
  "select": "?r",
  "where": { "@id": "ex:ClassA", "ex:restriction": "?r" }
}

returns e.g. "_:fdb-1751612345678901234-0-b0", which can then be edited directly:

{
  "where":  { "@id": "_:fdb-1751612345678901234-0-b0", "owl:someValuesFrom": "?old" },
  "delete": { "@id": "_:fdb-1751612345678901234-0-b0", "owl:someValuesFrom": "?old" },
  "insert": { "@id": "_:fdb-1751612345678901234-0-b0", "owl:someValuesFrom": { "@id": "ex:Gadget" } }
}

The parent’s reference to the node is untouched and the node keeps its identity across the edit.

The same ids work in SPARQL, in all of SELECT patterns, DELETE/INSERT templates, DELETE DATA, INSERT DATA, and DELETE WHERE:

DELETE { _:fdb-1751612345678901234-0-b0 owl:someValuesFrom ?old }
INSERT { _:fdb-1751612345678901234-0-b0 owl:someValuesFrom ex:Gadget }
WHERE  { _:fdb-1751612345678901234-0-b0 owl:someValuesFrom ?old }

Notes:

  • Only labels beginning with the reserved _:fdb- prefix behave this way. Ordinary client-authored labels (_:b0) keep standard RDF semantics: a fresh node per transaction on the write side, and an existential variable in SPARQL WHERE patterns. Clients cannot accidentally collide with the reserved space — transaction skolemization wraps client labels with a transaction id before prefixing fdb-.

  • Strictly per spec, SPARQL forbids blank nodes in DELETE templates and treats WHERE-pattern labels as variables; accepting _:fdb- ids as constants is a deliberate Fluree extension (the same one Virtuoso’s nodeID:// refs and Jena’s <_:label> syntax provide).

  • Bulk import mints the same way, into the same reserved space, with the same editability. Its ids carry a document scope — _:fdb-d<14 chars>-<label>, where the scope identifies the source file and <label> is the label as written in it (_:fdb-d1t3k9x0abcdef-genid10). They are [0-9a-z-] only, so they are writable in SPARQL, Turtle and JSON-LD without escaping.

  • Ledgers imported by Fluree 4.1.4 or earlier hold the older import format, which embedded the ledger id and therefore : and / (_:fdb-lubm:main-1-genid10). Those ids are still addressable from JSON-LD, but the SPARQL grammar does not allow : inside a blank-node label, so they cannot be written in SPARQL syntax. Re-importing the source produces ids in the current format. There is no mixed-format ledger: import requires a fresh ledger, so a ledger’s import ids are all of one format.

  • To find out which document an import id came from, look it up in the ledger’s txn-meta graph. Each source document contributes one triple whose subject is its scope — the first 19 characters of the id, _:fdb- plus the 14-character scope:

    SELECT ?source WHERE {
      GRAPH <urn:fluree:my/ledger:main#txn-meta> {
        _:fdb-d1t3k9x0abcdef <https://ns.flur.ee/db#importSource> ?source
      }
    }
    

    --skolem-namespace on fluree create --from controls the other half of the id: by default the ledger id salts the mint, so two ledgers loaded from one source tree hold different blank nodes; passing the same namespace to both makes them mint identical ids instead. See Blank nodes for what counts as a document — in particular for .jsonl/.ndjson, where a @context switch ends one document and starts the next.

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

FeatureWHERE/DELETE/INSERTReplace Mode
GranularityValue-levelPredicate-level (every value, every language)
Other propertiesPreservedPreserved
ConditionalYes (with filters)No
Pattern matchingYesNo
IdempotentDepends on logicYes, with one exception (see Upsert)
Use casePartial updatesComplete 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);
}