Skip to content

triples_update: a blank node on both sides of an update must render as one variable #6

Description

@EHoffm

This was generated by AI during triage.

What to build

triples_update builds two separate blank-node → variable maps, one for the DELETE patterns
(oldbn) and one for the INSERT patterns (newbn), and then binds every new-side variable with
BIND(BNODE() AS ?newbnN).

The consequence: a blank node that appears on both sides of an update — that is, the same node,
retained while one of its properties changes — is rendered as two different variables, and the INSERT
side always mints a brand-new store node. It is therefore impossible for any caller to update a
property of an existing blank node while keeping the node; the old one is unlinked and a fresh one
takes its place, orphaning any triple the caller did not include in old_triples.

A BNode present in both old_triples and new_triples is the same node and must render as the
same variable.

Direction. One shared map across both pattern sets. Emit BIND(BNODE() AS ?v) only for blank
nodes that occur exclusively in new_triples; a blank node also present in old_triples is
already bound by the WHERE clause and must reuse that binding.

Illustration. Given _:b on both sides with only its value changing, today's query is

DELETE { ?oldbn1 :hasValue 12.1 }
INSERT { ?newbn1 :hasValue 1.4 }
WHERE  { ?oldbn1 :hasValue 12.1 . BIND(BNODE() AS ?newbn1) }

which deletes the property from the existing node and creates a different node carrying the new
value. It should be

DELETE { ?bn1 :hasValue 12.1 }
INSERT { ?bn1 :hasValue 1.4 }
WHERE  { ?bn1 :hasValue 12.1 }

Acceptance criteria

  • A BNode occurring in both old_triples and new_triples renders as one variable in the
    generated query, used in both the DELETE and the INSERT block.
  • BIND(BNODE() AS ?v) is emitted only for blank nodes exclusive to new_triples.
  • Pure additions, pure removals and unequal-length replacements behave exactly as they do now.
  • The update remains a single atomic DELETE ... INSERT ... WHERE transaction — the intermediate
    state must never be observable, which is what the existing SHACL-correctness comment relies on.
  • A test asserts the generated query text for the shared-blank-node case, so this cannot silently
    regress. (The existing update tests require a live GraphDB; a query-construction test does not
    and is the cheaper guard.)

Context

Found while designing anonymous-node identity in SAWeindel/kapps_ogm — see
docs/prd/kapps-ogm-anonymous-node-identity.md (requirement R11) in
EHoffm/kapps_semantic_middleware. The fix is needed there whenever a blank node must survive an
update; it is a correctness fix for this library independent of that work.

I have a branch with the change and a query-construction test but only read access to this repository,
so I cannot push it — happy to hand it over or open a PR from a fork if that is easier.

Blocked by

None — can start immediately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions