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
10 changes: 7 additions & 3 deletions docs/concepts/iri-and-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,13 @@ For programmatic use from Rust, transactions can also set `TxnOpts.strict_compac
```

Blank nodes are:
- Local to a single transaction
- Cannot be referenced across transactions
- Useful for temporary or anonymous data
- Skolemized at insert time into the reserved `_:fdb-...` label space, which
queries return as the node's `@id`
- Addressable afterwards via that stable `_:fdb-...` id in queries and
transactions (see
[Editing blank-node structures](../transactions/update-where-delete-insert.md#editing-blank-node-structures-stable-_fdb--ids))
- Client-authored labels (`_:b0`) stay local to a single transaction — reusing
the same label later mints a new node

## Best Practices

Expand Down
5 changes: 4 additions & 1 deletion docs/transactions/insert.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,10 @@ Create entities without explicit IRIs:
}
```

Fluree generates a unique IRI for the blank node address.
Fluree generates a unique IRI for the blank node address, in the reserved
`_:fdb-...` label space. Queries return that id, and it can be used later to
address the node directly — see
[Editing blank-node structures](update-where-delete-insert.md#editing-blank-node-structures-stable-_fdb--ids).

## Adding to Existing Entities

Expand Down
59 changes: 59 additions & 0 deletions docs/transactions/update-where-delete-insert.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,65 @@ Calculate new values based on old:
}
```

## 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`:

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

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

```json
{
"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`:

```sparql
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).
- Some stable ids minted by bulk import embed `:` characters (e.g.
`_:fdb-lubm:main-1-genid10`). These are addressable from JSON-LD, but the
SPARQL grammar does not allow `:` inside blank-node labels, so such ids
cannot be written in SPARQL syntax.

## Error Handling

### No Match
Expand Down
5 changes: 3 additions & 2 deletions fluree-db-api/src/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4389,8 +4389,9 @@ where
let result = if chunk_source.is_trig(i) || chunk_source.is_nquads(i) {
// TriG (and N-Quads, converted to TriG) use the dedicated
// commit function for named-graph handling. It allocates
// codes in state.ns_registry; sync them to shared_alloc
// afterward for subsequent chunks' spool writes.
// codes through spool_config.ns_alloc (spool prefix
// lookups happen mid-parse); the sync below is a
// belt-and-braces consistency check.
let nq_converted;
let trig_content: &str = if chunk_source.is_nquads(i) {
nq_converted = fluree_db_transact::parse::nquads_to_trig(&content)
Expand Down
51 changes: 51 additions & 0 deletions fluree-db-api/src/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,11 @@ fn convert_named_graphs_to_templates(
match term {
RawTerm::Iri(iri) => {
if let Some(local) = iri.strip_prefix("_:") {
// Stable Fluree blank-node ids address the existing node;
// other labels skolemize fresh at staging.
if let Some(sid) = fluree_db_transact::stable_blank_node_sid_from_label(local) {
return Ok(TemplateTerm::Sid(sid));
}
Ok(TemplateTerm::BlankNode(local.to_string()))
} else {
Ok(TemplateTerm::Sid(ns_registry.sid_for_iri(iri)))
Expand All @@ -1670,6 +1675,11 @@ fn convert_named_graphs_to_templates(
match obj {
RawObject::Iri(iri) => {
if let Some(local) = iri.strip_prefix("_:") {
// Stable Fluree blank-node ids resolve to the stored node
// (see convert_term).
if let Some(sid) = fluree_db_transact::stable_blank_node_sid_from_label(local) {
return Ok((TemplateTerm::Sid(sid), None));
}
Ok((TemplateTerm::BlankNode(local.to_string()), None))
} else {
Ok((TemplateTerm::Sid(ns_registry.sid_for_iri(iri)), None))
Expand Down Expand Up @@ -3041,3 +3051,44 @@ impl crate::Fluree {
fn _ensure_error_used(e: ApiError) -> ApiError {
e
}

#[cfg(test)]
mod tests {
use super::*;
use fluree_db_transact::{RawObject, RawTerm, RawTriple};

/// TriG named-graph blocks (upsert/insert-turtle path): a stable
/// `_:fdb-...` id must resolve to the stored node's Sid, while ordinary
/// labels stay `TemplateTerm::BlankNode` for fresh skolemization at
/// staging.
#[test]
fn named_graph_stable_blank_node_resolves_to_sid() {
let block = NamedGraphBlock {
iri: "http://example.org/g1".to_string(),
triples: vec![RawTriple {
subject: Some(RawTerm::Iri("_:fdb-1234-0-b0".to_string())),
predicate: RawTerm::Iri("http://example.org/knows".to_string()),
objects: vec![RawObject::Iri("_:other".to_string())],
}],
prefixes: rustc_hash::FxHashMap::default(),
};
let mut ns = NamespaceRegistry::new();
let (templates, _delta) =
convert_named_graphs_to_templates(&[block], &mut ns).expect("convert");
assert_eq!(templates.len(), 1);

let expected = ns.blank_node_sid("1234-0-b0");
match &templates[0].subject {
TemplateTerm::Sid(sid) => {
assert_eq!(sid, &expected, "stable id must address the stored node");
}
other => panic!("stable id must resolve to a Sid, got {other:?}"),
}
match &templates[0].object {
TemplateTerm::BlankNode(label) => {
assert_eq!(label, "other", "ordinary labels keep fresh-mint semantics");
}
other => panic!("ordinary label must stay BlankNode, got {other:?}"),
}
}
}
2 changes: 2 additions & 0 deletions fluree-db-api/tests/grp_transact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ mod it_concurrent_update_reconcile;
mod it_enforce_unique_upsert_indexed;
#[path = "it_raw_txn_parallel_upload.rs"]
mod it_raw_txn_parallel_upload;
#[path = "it_stable_blank_nodes.rs"]
mod it_stable_blank_nodes;
#[path = "it_transact.rs"]
mod it_transact;
#[path = "it_transact_conditional.rs"]
Expand Down
Loading
Loading