Sync (graph synchronization)
Sync makes a named graph’s contents exactly the payload you supply, committing only the delta. It is the “full replacement” verb for data whose source of truth lives outside Fluree — an ontology maintained in an editor, a reference table regenerated by a pipeline — where you want the graph to match the latest export and the commit to show exactly what changed.
| Insert | Upsert | Sync | |
|---|---|---|---|
| Asserts new triples | ✓ | ✓ | ✓ |
| Retracts changed values | — | for supplied (subject, predicate) pairs | ✓ |
| Retracts triples absent from the payload | — | — | ✓ |
| Scope | payload | payload’s subjects/predicates | one whole named graph |
Semantics
Given the graph’s current contents A and the payload B:
- retract
A − B - assert
B − A A ∩ Bproduces no flakes — unchanged facts do not appear in the commitA = Bproduces no commit (committed: false)
Sync is transactional and history-preserving: one normal commit at
t = current + 1; queries as-of an earlier t still see the previous
contents. SHACL validation, modify-policy enforcement, and novelty
backpressure apply exactly as for any other transaction.
The scope is exactly one named graph, named by the caller — never inferred from the payload. The payload may not address named graphs itself, and reserved system graphs (and, for now, the default graph) are rejected.
HTTP endpoint
curl -X POST "http://localhost:8090/v1/fluree/sync?ledger=mydb:main&graph=http://example.org/graphs/ontology" \
-H "Content-Type: application/json" \
-d '{
"@context": { "ex": "http://example.org/" },
"@graph": [
{ "@id": "ex:alice", "ex:name": "Alice", "ex:role": "engineer" },
{ "@id": "ex:bob", "ex:name": "Bob" }
]
}'
Query parameters:
| Parameter | Meaning |
|---|---|
ledger | Target ledger (name:branch) |
graph | Required. Target graph IRI — the sync scope |
dryRun=true | Stage and report the delta (asserted/retracted counts) without committing — under the same policy, header, and inline-constraint inputs the real run uses, so it reports what the real run would do (or fails the way it would) |
allowEmpty=true | Confirm an explicitly empty payload ("@graph": []), which clears the graph |
The payload is JSON-LD (application/json). Convert Turtle exports
client-side (e.g. fluree-graph-turtle’s parse_to_json) for now.
A dry run responds with the delta report:
{ "ledger": "mydb:main", "graph": "http://example.org/graphs/ontology",
"asserted": 2, "retracted": 2, "committed": false, "dryRun": true, "t": 7 }
Rust API
#![allow(unused)]
fn main() {
use fluree_db_api::SyncGraphOpts;
let report = fluree
.sync_named_graph("mydb:main", "http://example.org/graphs/ontology",
&payload, SyncGraphOpts::default())
.await?;
assert!(report.committed || (report.asserted == 0 && report.retracted == 0));
}
SyncGraphOpts { dry_run, allow_empty } mirror the query parameters;
sync_named_graph_with additionally takes explicit TxnOpts and a
PolicyContext. The builder form
fluree.stage(&handle).sync_graph(graph_iri, &payload) is also available
(note: the builder does not apply the allow_empty gate). Its consensus
terminal build_commit() returns Ok(None) for a no-change sync. Target
validation (absolute IRI, no system graphs) is enforced at staging, so it
applies on every entry point.
Safety rails
- Empty payload requires opt-in.
"@graph": []means “the graph’s desired contents are empty” — i.e. clear the graph. WithoutallowEmpty, it is rejected, so a truncated export cannot silently wipe the graph. - Explicit scope. No graph parameter, no sync. Subjects in the payload never widen or narrow the scope.
- Dry run first. For a periodic pipeline, a
dryRuncall that reports an unexpectedly largeretractedcount is a cheap tripwire before the real run. - Policy model. Like
CLEAR/COPY/MOVE(and unlike DELETE-WHERE), the current-contents scan is not view-policy filtered: sync is an authoritative whole-graph replacement, and a view-filtered scan would leave rows the caller cannot see in place, breaking “the graph now equals the payload”. Modify-policy is still enforced on the resulting delta.
Blank nodes
Sync skolemizes the payload’s blank nodes with a deterministic, graph-scoped key: the same blank-node label in the same target graph mints the same skolem IRI on every sync. Exporters that keep labels stable (hand-maintained Turtle, most pipeline generators) therefore resync bnode-rooted structures (OWL restrictions, RDF lists) with zero churn.
Exporters that regenerate labels on every save (e.g. Protégé’s genid…)
still churn those structures: the triples are isomorphic but the labels — and
therefore the skolemized identities — differ. The result is correct, just
noisier commits. Structural (RDFC 1.0-style) canonicalization is the designed
follow-up for label-unstable exporters.
Scale
Staging a sync materializes the target graph’s current flakes plus the
payload’s flakes in memory — the same profile as CLEAR/COPY/MOVE
(chunked staging for whole-graph operations is a known follow-up). A huge
payload with a small delta is fine; the commit only carries the delta. A huge
delta is bounded by novelty backpressure (reindex_max_bytes): the commit
fails with NoveltyWouldExceed rather than overrunning memory, and the graph
is left unchanged.
A huge graph is bounded by FLUREE_MAX_GRAPH_SCAN_FLAKES (default
10,000,000; 0 disables): the whole-graph scan stops there and the
transaction fails with a resource-limit error naming the knob, instead of
exhausting memory — an identical resync of a large graph is the worst case,
since it materializes everything and commits nothing. A dry run trips the
cap the same way the real run would, and CLEAR/DROP/COPY/MOVE share
it. The follow-up that streams the diff instead of materializing the graph
is #1691.