Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8024ea1
bench(api): transact_upsert_replace measures upsert over existing values
aaj3f Sep 30, 2026
11bcd03
fix(query): a range probe through an empty overlay leaves the transla…
aaj3f Sep 30, 2026
73f5223
feat(transact): CurrentFacts reads the facts a retraction may name
aaj3f Sep 30, 2026
6d793a3
fix(transact): upsert retracts the values it replaces as they are stored
aaj3f Sep 30, 2026
06983c3
refactor(transact): graph sync and CLEAR/COPY/MOVE retract through Cu…
aaj3f Sep 30, 2026
fa5476b
fix(transact): a DELETE retracts only facts that are stored
aaj3f Sep 30, 2026
9d0a6ee
fix(transact): the annotation cascade retracts stored bundles in thei…
aaj3f Sep 30, 2026
7305a4a
fix(api): conflict retractions read index-resident named-graph values
aaj3f Sep 30, 2026
9d9d7e5
fix(api): a revert does not invert a legacy upsert phantom
aaj3f Sep 30, 2026
6e4aac8
docs(transactions): upsert replaces stored values; a DELETE retracts …
aaj3f Sep 30, 2026
b06e214
test(api): SPARQL DELETE DATA of a typed literal deletes the stored fact
aaj3f Sep 30, 2026
0d944b0
fix(transact): a DELETE names an indexed fact by its index key
aaj3f Sep 30, 2026
0b05671
fix(transact): modify policy judges every delete intent, stored or not
aaj3f Sep 30, 2026
b336fc2
fix(transact): a witness reads the graph its template writes, OPTIONA…
aaj3f Sep 30, 2026
a693f2c
docs(transactions): upsert is retry-safe except for values stored wit…
aaj3f Sep 30, 2026
4157948
bench(api): insert_formats measures Turtle upsert and mixed TriG insert
aaj3f Sep 30, 2026
45994cb
feat(transact): locate a TriG document's graph blocks from tokens
aaj3f Sep 30, 2026
1ba7436
feat(transact): parse RDF text once into transaction templates
aaj3f Sep 30, 2026
159284d
fix(api): upsert RDF text from one parse, without a JSON-LD round trip
aaj3f Sep 30, 2026
9727621
fix(api): a TriG insert parses once into templates
aaj3f Sep 30, 2026
ab3a048
fix(api): graph sync and graph insert parse RDF text once
aaj3f Sep 30, 2026
d4da410
refactor(api): delete the TriG round trip from the transaction lanes
aaj3f Sep 30, 2026
442a257
fix(cli): sync sends RDF text as written
aaj3f Sep 30, 2026
6ac5c48
test(sparql): TriG named graphs load in the W3C triple-term tests
aaj3f Sep 30, 2026
ddb898a
docs(transactions): RDF text is read once, the same way on every lane
aaj3f Sep 30, 2026
999d28c
perf(api): a TriG insert locates its document once
aaj3f Sep 30, 2026
8501b56
fix(transact): a ledger id in a GRAPH block is stored as written
aaj3f Sep 30, 2026
7226c78
fix(transact): a relative GRAPH block label is kept as written
aaj3f Sep 30, 2026
aa45a4a
perf(transact): a TriG document is lexed once, and its blocks share o…
aaj3f Sep 30, 2026
7cf46ea
docs(transactions): the orphaned blank-node query has a per-graph form
aaj3f Sep 30, 2026
987a937
fix(transact): a DELETE can name a stored ill-typed literal
aaj3f Sep 30, 2026
fe67076
docs(transactions): every idempotence claim names the one exception
aaj3f Sep 30, 2026
895e6f8
test(api): OPTIONAL and UNION DELETE rows take the witnessed lane on …
aaj3f Sep 30, 2026
11b5872
docs(transactions): the orphan queries count references from every graph
aaj3f Sep 30, 2026
99d5776
fix(transact): modify policy judges a restated value the identity can…
aaj3f Oct 1, 2026
e41be38
Merge remote-tracking branch 'origin/main' into fix/write-fidelity-up…
bplatz Oct 11, 2026
6ea6da7
Merge remote-tracking branch 'origin/main' into fix/write-fidelity-up…
bplatz Oct 11, 2026
007aa29
fix: address review feedback on stored-fact retraction and RDF text
bplatz Oct 11, 2026
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
3 changes: 2 additions & 1 deletion BENCHMARKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,12 @@ this table, so it is the one that rots. Add a row when you add a bench file.

| Crate | Bench file | Topic |
|---|---|---|
| `fluree-db-api` | `insert_formats.rs` | JSON-LD vs Turtle insert throughput, matrix of (format × txn count × nodes/txn) |
| `fluree-db-api` | `insert_formats.rs` | JSON-LD vs Turtle insert throughput, matrix of (format × txn count × nodes/txn); also Turtle upsert (new subjects and over existing ones) and TriG insert with a trailing graph block |
| `fluree-db-api` | `vector_query.rs` | End-to-end vector similarity through the query engine, 1K/5K articles, 768-dim |
| `fluree-db-api` | `fulltext_query.rs` | Full-text query through novelty + index |
| `fluree-db-api` | `import_bulk.rs` | Bulk Turtle import via `fluree.create(id).import(path).execute()`; single- vs default-threaded |
| `fluree-db-api` | `transact_commit.rs` | Single-commit latency on a fresh and a populated ledger (`iter_batched` setup) |
| `fluree-db-api` | `transact_upsert_replace.rs` | Upsert that replaces existing values: default/named graph × novelty/indexed, lang + `@list`, 16-predicate subjects, novelty-heavy base, and the new-subject control |
| `fluree-db-api` | `query_cold_reload.rs` | File-backed cold reload (load only, and load + first query) |
| `fluree-db-api` | `reindex_full.rs` | `Fluree::reindex(...)` end-to-end against a single-txn populated ledger |
| `fluree-db-api` | `reindex_incremental.rs` | Orchestrator's incremental path via `Fluree::trigger_index(...)` over delta novelty |
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.

13 changes: 9 additions & 4 deletions docs/cli/server-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,14 @@ MATCH (n:Person {id: 7}) RETURN n

Makes one named graph's contents exactly the payload, committing only the
delta. Data-bearer auth (same bracket as `/insert` / `/upsert`), not admin.
The CLI converts Turtle to JSON-LD client-side and sends it as
`application/json`; TriG, which that conversion cannot carry, is sent as
`application/trig`. A dry run answers with a delta report and
The CLI sends RDF text as written: Turtle as `text/turtle`, TriG as
`application/trig`, JSON-LD as `application/json`. A server from before
`/sync` read RDF bodies refuses a Turtle body, with `415 Unsupported Media
Type` or (v4.2.0 and v4.2.1) a `400` whose message ends `convert Turtle
payloads client-side`; for that server the CLI converts the Turtle to
JSON-LD client-side, sends it again, and prints a warning. Any other `400`
is reported as is, and TriG is never
converted. 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 —
Expand Down Expand Up @@ -1518,7 +1523,7 @@ Content-Type: application/json
| `default` (query) | No | Bare key: sync the default graph, said explicitly. Passing it with `graph` is a `400`. The CLI omits `graph` for the default graph. |
| `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 | The graph's desired full contents: insert-shaped JSON-LD, or Turtle / N-Triples / TriG by `Content-Type` (see [Payload formats](../transactions/sync.md#payload-formats)). The CLI sends JSON-LD for JSON-LD and Turtle (converted client-side), so those work against servers that predate RDF bodies; it sends TriG as `application/trig`. Policy headers / `opts` injection follow the [Policy Enforcement Contract](#policy-enforcement-contract). |
| body | Yes | The graph's desired full contents: insert-shaped JSON-LD, or Turtle / N-Triples / TriG by `Content-Type` (see [Payload formats](../transactions/sync.md#payload-formats)). The CLI sends Turtle as `text/turtle` and TriG as `application/trig`, converting Turtle to JSON-LD client-side only when a server that predates RDF bodies refuses it (a `415`, or v4.2.0 / v4.2.1's `400`). Policy headers / `opts` injection follow the [Policy Enforcement Contract](#policy-enforcement-contract). |

### Auth

Expand Down
24 changes: 15 additions & 9 deletions docs/cli/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,21 @@ produce no flakes, and an identical payload produces **no commit**. History
is preserved. See [Sync (graph synchronization)](../transactions/sync.md)
for the full semantics, safety rails, and blank-node behavior.

Turtle input is converted to JSON-LD client-side before submission, so a
Turtle export works against any server that implements the `/sync`
endpoint, including servers from before it accepted Turtle bodies. A `.nt`
file is read as Turtle, of which N-Triples is a subset.

TriG is sent as TriG rather than converted. It is recognized by a `.trig`
name or `--format trig`, and also when a body read as Turtle turns out to
hold graph blocks, as when it is piped in. Its blocks must name the
`--graph` target, and cannot sit beside default-graph triples (see
Turtle input is sent as written and parsed where it is staged, the way
every write reads RDF text (see
[How RDF text is read](../transactions/turtle.md#how-rdf-text-is-read)).
A `.nt` file is read as Turtle, of which N-Triples is a subset. With
`--remote`, a server from before `/sync` accepted Turtle bodies refuses
one: v4.2.0 and v4.2.1 answer 400 (`convert Turtle payloads client-side`),
others 415. For that server the CLI converts the Turtle to JSON-LD, sends
it again, and prints a warning. The conversion loses collection order and
refuses some IRI schemes (`tag:`, `kb:`), so upgrading the server is the
better fix.

TriG is sent as TriG. It is recognized by a `.trig` name or
`--format trig`, and also when a body read as Turtle turns out to hold
graph blocks, as when it is piped in. Its blocks must name the `--graph`
target, and cannot sit beside default-graph triples (see
[payload formats](../transactions/sync.md#payload-formats)):

```bash
Expand Down
8 changes: 7 additions & 1 deletion docs/concepts/datatypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,11 +406,17 @@ Numeric types (long, double, bigint, decimal) are mutually comparable via automa

A literal such as `"2024-02-30"^^xsd:date` or `"300"^^xsd:byte` names a
built-in datatype but is not one of its values. JSON-LD transactions and
SPARQL UPDATE reject it, naming the datatype in the error. Turtle
SPARQL UPDATE refuse to insert it, naming the datatype in the error. Turtle
transactions and bulk import keep it, as RDF allows: the literal is stored with
its text and its datatype and reads back exactly as written, but it is not a
date or a number, so it never equals one.

A delete names such a literal as it is stored, so `DELETE DATA` or a JSON-LD
`delete` with the same text and datatype removes it, and one that matches
nothing stored commits nothing. A constant in a WHERE clause or a query is
still refused, so `DELETE WHERE` and `SELECT` reach a stored one only through
a variable (`FILTER(STR(?o) = "2024-02-30")`).

### Type Casting in Queries

SPARQL provides functions for type conversion:
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/edge-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ If a fact is naturally about a *node* (Alice's birthdate, Acme's industry), put
|---|---|---|
| **JSON-LD insert / upsert / update** | `@annotation` (or alias `@edge`) on a value object | Most ergonomic. Covers literal-valued edges (with explicit `@type` / `@language`), parallel annotations, named reifiers, body cascades, and **named-graph edges** (an annotation on an edge inside a named graph is written into that same graph, keeping the edge's graph identity). `@reifies` is a query-side construct, **not** an insert form (user-authored `@reifies` on a write is rejected). |
| **SPARQL 1.2 UPDATE** | `INSERT DATA { :s :p :o {\| ... \|} }`, `~ <reifier>`, optional `INSERT { } WHERE { }` templates | Use this when integrating with SPARQL pipelines or when porting from RDF 1.2 / SPARQL-star. **Default graph only:** an annotation tail inside an explicit `GRAPH { }` block, or under a `WITH <g>` template, is rejected — use the JSON-LD surface or TriG-star for named-graph edge annotations. See [SPARQL 1.2 surface](#sparql-12--rdf-12-surface) below for the per-operation rules. |
| **Turtle / N-Triples / TriG / N-Quads ingest** (`insert`, `upsert`, bulk `import`, graph sync (`fluree sync`, `/sync`), memory import; TriG via `insert` / `upsert` / `import` / `fluree sync` / `/sync`) | RDF 1.2 asserting forms: `:s :p :o ~ <reifier> {\| ... \|}`, `<< :s :p :o ~ :r >>` in subject or object position, and the canonical `:r rdf:reifies <<( :s :p :o )>>` (the only star spelling N-Triples and N-Quads have) — in the default graph and inside TriG `GRAPH { }` blocks alike | Same on-disk bundle as `@annotation`. An annotation inside a `GRAPH { }` block, or on an N-Quads statement with a graph label, is written into that graph with the edge's graph identity, exactly as JSON-LD `@graph` + `@annotation` does. Every form asserts the base edge (Fluree reifies asserted edges, so `<< s p o >>` and `rdf:reifies <<( s p o )>>` are asserting here). Rejected with a specific error: `<<( ... )>>` anywhere other than the object of `rdf:reifies`, nested triple terms, star constructs inside an annotation body, annotations on collections, and annotations in a TriG `<#txn-meta>` block. Paths that convert to JSON-LD first (`upsert`, `graph sync`, memory import) also reject an annotation on an `rdf:type` edge. See [Turtle ingest](../transactions/turtle.md#edge-annotations-rdf-12--turtle-star). |
| **Turtle / N-Triples / TriG / N-Quads ingest** (`insert`, `upsert`, bulk `import`, graph sync (`fluree sync`, `/sync`), memory import; TriG via `insert` / `upsert` / `import` / `fluree sync` / `/sync`) | RDF 1.2 asserting forms: `:s :p :o ~ <reifier> {\| ... \|}`, `<< :s :p :o ~ :r >>` in subject or object position, and the canonical `:r rdf:reifies <<( :s :p :o )>>` (the only star spelling N-Triples and N-Quads have) — in the default graph and inside TriG `GRAPH { }` blocks alike | Same on-disk bundle as `@annotation`. An annotation inside a `GRAPH { }` block, or on an N-Quads statement with a graph label, is written into that graph with the edge's graph identity, exactly as JSON-LD `@graph` + `@annotation` does. Every form asserts the base edge (Fluree reifies asserted edges, so `<< s p o >>` and `rdf:reifies <<( s p o )>>` are asserting here). Rejected with a specific error: `<<( ... )>>` anywhere other than the object of `rdf:reifies`, nested triple terms, star constructs inside an annotation body, annotations on collections, and annotations in a TriG `<#txn-meta>` block. The memory import converts Turtle to JSON-LD first and also rejects an annotation on an `rdf:type` edge; the transaction endpoints accept one like any other edge. See [Turtle ingest](../transactions/turtle.md#edge-annotations-rdf-12--turtle-star). |

Annotations are backed by a set of [reserved system predicates](../reference/vocabulary.md#edge-annotation-predicates-reserved) that application writes can't author by hand — mint annotations through `@annotation` / `@edge` (JSON-LD) or the RDF 1.2 forms (`~`, `{| |}`, `<< >>`, `rdf:reifies <<( )>>`) in SPARQL UPDATE and Turtle. (Bulk import is an administrative bootstrap path that may ingest already-lowered annotation bundles without up-front validation; see the [storage-internals design doc](../design/edge-annotations.md) for how malformed bundles are handled.)

Expand Down
5 changes: 2 additions & 3 deletions docs/ledger-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,8 @@ User queries against the config graph go through **policy enforcement**. If `f:d
The config graph is written and queried through normal CLI transaction and query commands:

```bash
# Write config via SPARQL UPDATE. TriG through `fluree insert` works too, but
# not with the anonymous blank nodes (`[ … ]`) config settings are usually
# written as (#1930); see "Writing from the CLI" in writing-config.md.
# Write config via SPARQL UPDATE. TriG through `fluree insert` or
# `fluree upsert` works too; see "Writing from the CLI" in writing-config.md.
fluree update --ledger mydb:main --format sparql -e '
PREFIX f: <https://ns.flur.ee/db#>
INSERT DATA {
Expand Down
2 changes: 1 addition & 1 deletion docs/ledger-config/writing-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ GRAPH <urn:fluree:mydb:main#config> {

## Writing from the CLI

`fluree insert` and `fluree upsert` read TriG, but the recipe above writes its settings as anonymous blank nodes (`[ … ]`) inside the `GRAPH` block, which those commands do not read yet ([#1930](https://github.com/fluree/db/issues/1930)). Write the config graph with SPARQL UPDATE through `fluree update` instead:
`fluree insert` and `fluree upsert` read the TriG recipe above as written, anonymous blank nodes (`[ … ]`) inside the `GRAPH` block included. The same settings can also be written with SPARQL UPDATE through `fluree update`:

```bash
fluree update -l mydb:main --format sparql -e '
Expand Down
4 changes: 4 additions & 0 deletions docs/security/policy-in-transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ Every modification path runs the same `f:modify` policy check on its staged flak

Crucially, the policy is checked against the **flakes**, not the operation type. A transaction that retracts a flake the identity can't modify is rejected just like an insert that asserts one.

A write that restates a stored value as it is stages nothing: an upsert of an unchanged value, a DELETE and INSERT of the same value, or an `ADD`, `COPY` or `MOVE` of a fact the destination graph already holds. When the identity cannot view that value, the restatement is still checked against `f:modify`, so it is refused unless the identity may modify the value, whether or not the value is stored. Restating a value the identity can view commits nothing, with no modify check.

**Modify without view.** Granting `f:modify` on a value without `f:view` does not keep that value confidential from the identity: an identity that may modify a property can tell whether a given value is stored by writing it. Configure such policies with care.

Enforcement is also independent of the **wire format**: the check runs on the staged flakes, so JSON-LD, SPARQL UPDATE, and Turtle / TriG / N-Triples writes are all governed by the same `f:modify` policy. Sending data as Turtle is not a way to bypass write policy.

Class targeting on the write side is **exact**: an `f:onClass` modify policy governs *every* flake whose subject is an instance of the class — including properties the class has never carried before. (Read-side class targeting uses the committed class→property index; write-side targeting matches by the subject's classes directly, since a write may introduce class/property combinations no committed data predicts.)
Expand Down
3 changes: 2 additions & 1 deletion docs/transactions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Adding new data to the database:

### [Upsert](upsert.md)

Idempotent transactions that replace values for supplied predicates:
Transactions that replace values for supplied predicates, idempotent except for values stored with less detail than they were written:
- Upsert semantics
- Use cases for upsert
- Idempotent operations
Expand Down Expand Up @@ -285,6 +285,7 @@ For idempotent transactions:
- Include unique identifiers
- Design for retry safety
- Use deterministic IRIs
- Write values at the precision they are stored with: `xsd:integer` for integers beyond 64 bits, at most six fractional digits for `dateTime` and `time` (see [Upsert](upsert.md#idempotency))

## Best Practices

Expand Down
27 changes: 24 additions & 3 deletions docs/transactions/retractions.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,35 @@ Remove entities with specific property:

### Idempotent

Retracting a non-existent triple is a no-op:
Retracting a triple that is not stored is a no-op, and a transaction that
retracts nothing else commits nothing:

```text
t=1: No triple exists
t=2: DELETE { ex:alice schema:age 30 }
Result: No change (triple didn't exist)
t=2: DELETE DATA { ex:alice schema:age 30 }
Result: nothing is committed; the ledger stays at t=1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't hold for a graph the ledger doesn't have yet. DELETE DATA { GRAPH <unregistered> { … } } makes a commit with flake count 0 that advances t and registers the graph.

```

A DELETE names RDF terms exactly. The datatype must match the stored one
(`30` is `xsd:integer`, so it does not retract `"30"^^xsd:int`); a language
tag matches case-insensitively (`"a"@EN` retracts a stored `"a"@en`). A
`DELETE`/`INSERT` of a triple that is not stored therefore inserts it — the
retraction names nothing, so it cannot cancel the insertion (SPARQL 1.1
Update's `(DS − D) ∪ I`):

```sparql
DELETE { ex:alice schema:age 30 } INSERT { ex:alice schema:age 30 } WHERE {}
```

stores `ex:alice schema:age 30` if it was absent, and commits nothing if it
was present.

Once a fact is indexed, a DELETE is matched against what the index stores,
which is less than was written for two kinds of value: an integer too large
for 64 bits keeps no XSD subtype, and a `dateTime` or `time` keeps six
fractional digits. A DELETE naming the same big integer under another integer
datatype, or the same instant to the microsecond, retracts such a fact.

### No Cascading by Default

Retracting an entity doesn't automatically retract references to it:
Expand Down
Loading
Loading