Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 77 additions & 2 deletions .fluree-memory/repo.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -1897,6 +1897,81 @@ mem:fact-01kv6sahj5se39hv5hkmjytznz a mem:Fact ;
mem:createdAt "2026-06-15T23:19:32.677756+00:00"^^xsd:dateTime ;
mem:rationale "Serial writes (summed S3 round-trips) blew the 15-min Lambda cap on a real 21 GB/74k-artifact DBLP restore; user chose the parallelize-writes fix. Sequential-stream-read ceiling is still unaddressed." .

mem:constraint-01m0sy4avrsbj4xs4tnaxw3xnr a mem:Constraint ;
mem:content "Every new TransactOperation variant MUST get an arm in `stage_under_lock` (tx_builder.rs), not only stage_plan / owned execute / owned stage. stage_under_lock is the path Raft (`build_commit_with_handle`) and every policy-gated, pre-built, SPARQL and Cypher local commit takes, and its JSON-like fallthrough silently stages an unknown op as `TxnType::Insert` into the default graph — the sync verb shipped that way until review." ;
mem:tag "new-verb-checklist" ;
mem:tag "raft" ;
mem:tag "stage-under-lock" ;
mem:tag "transact" ;
mem:scope mem:repo ;
mem:severity "must" ;
mem:artifactRef "fluree-db-api/src/tx_builder.rs" ;
mem:branch "feature/graph-sync-delta" ;
mem:createdAt "2026-08-24T13:07:35.420277+00:00"^^xsd:dateTime ;
mem:rationale "Local no-policy tests pass via the optimistic path, so the gap is invisible until raft or policy is involved; pinned by policy_gated_sync_targets_the_named_graph." .

mem:constraint-01m0sy4gbwwx44d2yc3kepwv5a a mem:Constraint ;
mem:content "`BodyKind` and `TransactionBody` are postcard-positional in persisted Raft state (`QueueEntry.body_kind` in state snapshots): append new variants at the END only (pinned by body_kind_ordinals_are_append_only). Separately, `RefTransactBuilder::build_commit` returns `Ok(None)` for a zero-flake update/upsert/sync that registers no graph; the raft worker republishes the current head with `install: None` (mirroring revert NoOp). Before this, a no-change write under Raft failed with EmptyTransaction and poisoned the queued request." ;
mem:tag "consensus" ;
mem:tag "no-op" ;
mem:tag "postcard" ;
mem:tag "raft" ;
mem:tag "wire-format" ;
mem:scope mem:repo ;
mem:severity "must" ;
mem:artifactRef "fluree-db-api/src/tx_builder.rs" ;
mem:artifactRef "fluree-db-consensus/src/lib.rs" ;
mem:artifactRef "fluree-db-consensus/src/raft/commit_worker.rs" ;
mem:branch "feature/graph-sync-delta" ;
mem:createdAt "2026-08-24T13:07:41.052490+00:00"^^xsd:dateTime ;
mem:rationale "Mid-enum insertion shifts every later ordinal so existing snapshots/mixed-version nodes decode the wrong operation; the no-op terminal is the only way consensus can express committed=false." .

mem:decision-01m0svwaqmb1h9dweex82pcxd7 a mem:Decision ;
mem:content "Graph sync (feature/graph-sync-delta) = Insert-typed Txn + `sync_graph: Option<IRI>` directive: staging adds a wave after the upsert wave pushing every scanned target-graph flake as a retraction; FlakeAccumulator::mixed nets A∩B to zero so the commit is exactly the delta. Chosen over a GraphMgmtOp variant (payload doesn't fit SPARQL-shaped ops) and over the materialize.rs text-diff pipeline (needs a built binary index + bulk-import path drops retractions; the accumulator path works on any live ledger and inherits policy/SHACL/cascade/no-op machinery). Retraction scan follows CLEAR's policy model (not view-filtered, O4). Zero-staged sync joins the Update|Upsert no-op skip via StageResult.sync_graph." ;
mem:tag "delta" ;
mem:tag "graph-sync" ;
mem:tag "named-graph" ;
mem:tag "staging" ;
mem:tag "transact" ;
mem:scope mem:repo ;
mem:artifactRef "docs/transactions/sync.md" ;
mem:artifactRef "fluree-db-api/src/admin.rs" ;
mem:artifactRef "fluree-db-api/tests/it_sync_graph.rs" ;
mem:artifactRef "fluree-db-transact/src/parse/jsonld.rs" ;
mem:artifactRef "fluree-db-transact/src/stage.rs" ;
mem:branch "feature/graph-sync-delta" ;
mem:createdAt "2026-08-24T12:28:15.988437+00:00"^^xsd:dateTime ;
mem:rationale "Whole-graph ops that materialize scans + payload are the CLEAR-class memory profile; chunked staging remains the known follow-up. Bnode stability = deterministic graph-scoped skolem_txn_id (sync+doc_scope(doc_id)) set in stage_sync_transaction_tracked — label-unstable exporters (Protégé genid) still churn; RDFC canonicalization is the designed seam." ;
mem:alternatives "GraphMgmtOp::Sync variant; CLEAR+INSERT relying on accumulator (no explicit no-op detection); offline export/external-sort/diff pipeline (v2 scale path)" .

mem:decision-01m0t021b2wwasg494g0yxre7w a mem:Decision ;
mem:content "`fluree sync` CLI grammar: top-level verb (not `graph sync`), target graph is the constant (`--graph <IRI>` required), the SOURCE of desired contents is pluggable via `commands/graph_sync.rs::SyncSource` (today RdfText; an R2RML-over-Iceberg/CSV/Excel source is one new variant that materializes to the same JSON-LD payload). Turtle converts client-side (endpoint is JSON-LD only). `--dry-run --json` is the scripting pre-flight; `--json` output = the server dry-run report shape on both local and remote paths. NOTE: `commands/sync.rs` already exists (fetch/pull/push/publish replication) — never reuse that name." ;
mem:tag "cli" ;
mem:tag "command-grammar" ;
mem:tag "graph-sync" ;
mem:tag "r2rml" ;
mem:scope mem:repo ;
mem:artifactRef "docs/cli/server-integration.md" ;
mem:artifactRef "docs/cli/sync.md" ;
mem:artifactRef "fluree-db-cli/src/commands/graph_sync.rs" ;
mem:branch "feature/graph-sync-delta" ;
mem:createdAt "2026-08-24T13:41:17.282471+00:00"^^xsd:dateTime ;
mem:rationale "User wants sync to later drive from Iceberg/CSV/Excel via R2RML (\"latest downstream\"); keeping the source a client-side seam means no new server endpoint or command per source, and --remote only moves where materialization happens." .

mem:fact-01m0svwr4r77f4v4xy9k7w5q6e a mem:Fact ;
mem:content "CommitReceipt now carries assert_count/retract_count (count_ops in commit.rs, populated in finalize_state_with_base from commit_record.flakes). Raft-applied receipts report 0/0 — AppliedReceipt/idempotency records don't persist the split (raft wire-format stability), so sync reports over raft lack the convenience counts. Sync's HTTP dry-run path computes counts locally instead. New verbs need ALL of: TransactionBody variant + operation_tag + body_hash domain tag + BodyKind + BOTH appliers (local.rs and raft/commit_worker.rs body dispatch) + server route family + tx_builder TransactOperation/OpPlan/stage_plan arms + BOTH no-op skip conditions (tx_builder owned execute + commit_and_finalize, and the three tx.rs transact paths)." ;
mem:tag "commit-receipt" ;
mem:tag "consensus" ;
mem:tag "new-verb-checklist" ;
mem:tag "transact" ;
mem:scope mem:repo ;
mem:artifactRef "fluree-db-api/src/tx_builder.rs" ;
mem:artifactRef "fluree-db-consensus/src/lib.rs" ;
mem:artifactRef "fluree-db-transact/src/commit.rs" ;
mem:branch "feature/graph-sync-delta" ;
mem:createdAt "2026-08-24T12:28:29.720563+00:00"^^xsd:dateTime ;
mem:rationale "The verb-seam checklist is easy to miss partially (a missed applier silently breaks the verb under raft only)." .

mem:fact-01kzemcff3b773aq7yckej9dhk a mem:Fact ;
mem:content "Local-filesystem Iceberg support (commit 15ca56807, E2E-verified against a real pyiceberg table): Direct table_location accepts file:///, file:/, and bare absolute paths. Adds FileIcebergStorage and an IcebergStorageBackend enum (S3|File) threaded through the api scan surface, session caches, and lazy path; the version-hint fallback lists metadata/ via a new list_files trait method. GOTCHA the E2E test caught: Direct location validation lives in TWO places — fluree_db_iceberg::config and fluree-db-api graph_source/config.rs validate() — and both must accept a new scheme." ;
mem:tag "direct-mode" ;
Expand Down Expand Up @@ -2664,7 +2739,7 @@ mem:fact-01kynrrhdfqt0n6rx1wpcv5843 a mem:Fact ;
mem:rationale "Answers \"would batching help existing-subject upserts?\" with measurements — avoids speculative batching work; documents the store-cached p_sid_table invariant (store immutable behind Arc → OnceLock safe)." .

mem:fact-01m0srrpntk3mxseprnpcvey08 a mem:Fact ;
mem:content "REVERTED, do not re-land as written: lowering star-block VALUES to FILTER(?v IN ...) at block assembly (convert_star_values_to_membership_filters) did not work. inline_singleton_values_objects rewrites a singleton VALUES into the triple object but RETAINS the VALUES pattern, so its var is no longer produced by any star triple, membership_filter_from_values declines it, and the all-or-nothing gate declines the whole block — which is exactly the two-VALUES repro shape. Measured: 0 firings across the 6 tests that shipped with it, 1 across the whole 349-test SPARQL group. Where it does fire it is SLOWER: Function::In yields no range constraint from extract_range_constraints, so the block carries no object_bounds, has_selective_anchor is false, and it leaves the fused PropertyJoinOperator for the NLJ chain. Push the constraint down as a SEED that keeps the star anchored, not a filter that unanchors it." ;
mem:content "REVERTED, do not re-land as written: lowering star-block VALUES to FILTER(?v IN ...) at block assembly failed two ways. inline_singleton_values_objects rewrites a singleton VALUES into the triple object but RETAINS the VALUES pattern, so membership_filter_from_values declines its var and the all-or-nothing gate declines the whole block — exactly the two-VALUES repro shape; 1 firing across the 349-test SPARQL group. Where it does fire it is SLOWER: Function::In yields no object_bounds, has_selective_anchor goes false, and the block leaves the fused PropertyJoinOperator for the NLJ chain. Push the constraint down as a SEED that keeps the star anchored, not a filter that unanchors it." ;
mem:tag "performance" ;
mem:tag "planner" ;
mem:tag "reverted" ;
Expand All @@ -2676,7 +2751,7 @@ mem:fact-01m0srrpntk3mxseprnpcvey08 a mem:Fact ;
mem:artifactRef "fluree-db-query/src/planner.rs" ;
mem:branch "fix/values-object-scan-constraint" ;
mem:createdAt "2026-08-24T11:33:51.418875+00:00"^^xsd:dateTime ;
mem:updatedAt "2026-08-25T23:36:20.896572+00:00"^^xsd:dateTime ;
mem:updatedAt "2026-08-26T13:52:55.183934+00:00"^^xsd:dateTime ;
mem:rationale "The approach looks obviously right and was already tried and reverted; the singleton-fold interaction and the missing object_bounds are the two things that kill it." .

mem:fact-01m0srrz87xpy9dy11z0b084hv a mem:Fact ;
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [graph](cli/graph.md)
- [insert](cli/insert.md)
- [upsert](cli/upsert.md)
- [sync](cli/sync.md)
- [update](cli/update.md)
- [load](cli/load.md)
- [query](cli/query.md)
Expand Down Expand Up @@ -134,6 +135,7 @@
- [Overview](transactions/overview.md)
- [Insert](transactions/insert.md)
- [Upsert](transactions/upsert.md)
- [Sync (graph synchronization)](transactions/sync.md)
- [Update (WHERE/DELETE/INSERT)](transactions/update-where-delete-insert.md)
- [Conditional updates (atomic / compare-and-swap)](transactions/conditional-updates.md)
- [Retractions](transactions/retractions.md)
Expand Down
32 changes: 32 additions & 0 deletions docs/api/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,38 @@ Both W3C TriG graph-block forms are accepted: the SPARQL-style keyword form
The compact form is what stock RDF tooling — rdflib, Apache Jena, RDF4J — emits
by default, so payloads generated by those libraries are ingested as-is.

### POST /sync

Synchronize a named graph: make its contents exactly the JSON-LD payload,
committing only the delta (`current − payload` retracted, `payload −
current` asserted; unchanged facts produce no flakes). An identical payload
produces no commit. See [Sync](../transactions/sync.md).

**URL:**
```
POST /sync?ledger={ledger-id}&graph={graph-iri}
POST /sync/{ledger-id}?graph={graph-iri}
```

**Query parameters:** `graph` (required target graph IRI), `dryRun=true`
(stage and report the delta without committing), `allowEmpty=true` (confirm
an explicitly empty payload, which clears the graph).

**Supported Content Types:**
- `application/json` - JSON-LD (Turtle payloads must be converted client-side for now)

**Example:**
```bash
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/ns/" },
"@graph": [
{ "@id": "ex:alice", "ex:name": "Alice" }
]
}'
```

### POST /push/*ledger

Push precomputed commit v2 blobs to the server.
Expand Down
3 changes: 2 additions & 1 deletion docs/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ fluree query 'SELECT ?name WHERE { ?s <http://example.org/name> ?name }'
| [`branch`](branch.md) | Branches: create, list, drop, rebase, merge, diff, revert |
| [`insert`](insert.md) | Insert data into a ledger |
| [`upsert`](upsert.md) | Upsert data (insert or update existing) |
| [`sync`](sync.md) | Make a named graph's contents exactly the supplied data, committing only the delta |
| [`update`](update.md) | Update with WHERE/DELETE/INSERT patterns |
| [`load`](load.md) | Stream a CSV into a ledger as batched Cypher/JSON-LD upserts (`LOAD CSV`) |
| [`query`](query.md) | Query a ledger |
Expand Down Expand Up @@ -147,7 +148,7 @@ When you run `fluree init`, a `.fluree/` directory is created with:

## Input Resolution

Commands that accept data input (`insert`, `upsert`, `update`, `query`) use flexible argument resolution:
Commands that accept data input (`insert`, `upsert`, `sync`, `update`, `query`) use flexible argument resolution:

| Arguments | Behavior |
|-----------|----------|
Expand Down
94 changes: 93 additions & 1 deletion docs/cli/server-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This document is for implementers building a custom server (for example in `../s

The CLI supports two broad categories of remote operations:

- **Data API**: query / update / insert / upsert / info / exists / show / log / history / context / explain, plus admin operations like create / drop / reindex / branch (create / drop / rebase / merge) / publish / export / import.
- **Data API**: query / update / insert / upsert / sync / info / exists / show / log / history / context / explain, plus admin operations like create / drop / reindex / branch (create / drop / rebase / merge) / publish / export / import.
- **Replication / sync**: clone / pull / fetch (content-addressed replication by CID, via pack + storage proxy), ledger-archive (`export --format ledger`), and wholesale restore (`create --remote --from <archive>.flpack`, via `POST /import`).

## Base URL And Discovery
Expand Down Expand Up @@ -251,6 +251,7 @@ See [Ledger portability](#ledger-portability-flpack-files) below for the on-disk
- `POST {api_base_url}/query/*ledger`
- `POST {api_base_url}/insert/*ledger`
- `POST {api_base_url}/upsert/*ledger`
- `POST {api_base_url}/sync/*ledger` — see [Sync Contract](#sync-contract).
- `POST {api_base_url}/update/*ledger`
- `GET {api_base_url}/info/*ledger`
- `GET {api_base_url}/exists/*ledger`
Expand Down Expand Up @@ -330,6 +331,20 @@ MATCH (n:Person {id: 7}) RETURN n
- Bearer ledger scope (`can_read`) and `Fluree-Min-T` apply as on the query
path. `--at` is rejected for remote Cypher explain (use `--direct`).

### `fluree sync --remote <name>` (graph synchronization)

- `POST {api_base_url}/sync/*ledger?graph=<iri>[&dryRun=true][&allowEmpty=true]`

Makes one named graph's contents exactly the JSON-LD payload, committing
only the delta. Data-bearer auth (same bracket as `/insert` / `/upsert`),
not admin. The CLI converts Turtle to JSON-LD client-side, so the endpoint
only ever sees `application/json`. A dry run answers with a delta report and
must commit nothing; a real run answers with the standard transact response.
Designed so the CLI's source of desired contents (today RDF text; later
R2RML-mapped Iceberg / CSV / spreadsheet data) is invisible to the server —
every source arrives as the same payload. See
[Sync Contract](#sync-contract).

### `fluree load` (CSV → batched upserts), `fluree update --format cypher`

`fluree load` streams a local CSV into a ledger as a sequence of batched
Expand Down Expand Up @@ -1408,6 +1423,83 @@ them all.
| Report struct | `fluree_db_api::DropNamedGraphReport` |
| Graph registry | `fluree_db_core::graph_registry` (system graph constants and IRI helpers) |

## Sync Contract

`fluree sync <ledger> --graph <iri> [--dry-run] [--allow-empty] --remote <name>`
issues:

```
POST {api_base_url}/sync/{ledger}?graph=urn%3Aexample%3Aontology[&dryRun=true][&allowEmpty=true]
Content-Type: application/json

{ "@context": { ... }, "@graph": [ ... desired full contents of the graph ... ] }
```

| Parameter | Required | Description |
|-----------|----------|-------------|
| `graph` (query) | Yes | Full **absolute** IRI of the target named graph (same validation rules as `/drop-graph`'s `graph`). The sync scope is exactly this graph — the payload must not address named graphs itself, and the ledger's `txn-meta` / `config` system graphs are rejected. |
| `dryRun` (query) | No | `true` → stage and report the delta; commit nothing. |
| `allowEmpty` (query) | No | `true` → accept an explicitly empty payload (`"@graph": []`), which clears the graph. Without it an empty payload is a `400`. |
| body | Yes | Insert-shaped JSON-LD describing the graph's desired full contents. The CLI always sends JSON-LD (Turtle is converted client-side). Policy headers / `opts` injection follow the [Policy Enforcement Contract](#policy-enforcement-contract). |

### Auth

Data-bearer auth, same bracket as `/insert` and `/upsert` (a token scoped to
the ledger with write access). Not admin.

### Required semantics

Given the graph's current contents `A` and the payload `B`:

1. Retract `A − B`, assert `B − A`; facts in `A ∩ B` produce no flakes.
2. Identical payload (`A = B`) → **no commit** and a successful response
whose `t` is the unchanged head.
3. One commit for the whole delta (`t = current + 1`); history preserved.
4. Policy, SHACL, and uniqueness constraints apply exactly as for a normal
transaction. The current-contents scan is an authoritative replacement
(not view-policy filtered) — a row the caller cannot see is still
retracted if absent from the payload; modify-policy is enforced on the
resulting delta.
5. A dry run stages under the **same** policy / option inputs as the real
run, so its counts (and its failures) predict the real run.
6. Blank nodes are skolemized with a deterministic, graph-scoped key so a
payload with stable labels resyncs bnode structures without churn.

### Response

Real run (`200 OK`): the standard transact response (`ledger`, `t`,
`tx-id`, commit info) — identical in shape to `/upsert`.

Dry run (`200 OK`):

```json
{
"ledger": "mydb:main",
"graph": "urn:example:ontology",
"asserted": 2,
"retracted": 2,
"committed": false,
"dryRun": true,
"t": 7
}
```

The CLI's `--json` output uses this same shape for both local and remote
runs, so scripts consume either path identically.

### Error responses

| Status | When |
|--------|------|
| `400` | missing `graph`; malformed / relative graph IRI; system-graph target; empty payload without `allowEmpty`; payload addressing named graphs; non-JSON body (Turtle/TriG are not accepted here) |
| `401` / `403` | per the policy contract |
| `404` | unknown ledger |

### Reference implementation

`fluree-db-server/src/routes/transact.rs` (`sync`, `sync_ledger`,
`sync_local`) and `Fluree::sync_named_graph_with` in `fluree-db-api`.

## Rebase Contract

`fluree branch rebase <branch> --remote <name>` issues:
Expand Down
Loading
Loading