OpenEnergyPlatform / OpenEnergyPlatform/oeplatform
Deleting a scenario bundle in the browser leaves its version node behind
@jh-RLI is already working on this.
Since Sep 10, 2026.
- Dominant language
- Python
- Stars
- 65
- Forks
- 29
- Avg merge
- 15h 25m
- Merged PRs (30d)
- 32
Description
Description of the issue
From #2438 onward, every scenario bundle the REST API writes carries a version node:
<.../oekg/version/{uid}> oekg:versionOf <.../oekg/{uid}> ;
oekg:version "3"^^xsd:integer ;
oekg:writeToken "<uuid>" .
The triple points at the bundle rather than away from it, and it has to: the bundle
shape is sh:closed with only rdf:type ignored, so a version hung off the bundle itself
would make every bundle the API writes shape-invalid. That direction is what lets the
guard work without editing the shape, and it is also what keeps the version out of read
payloads for free.
The consequence is on the delete side. delete_factsheet_by_id_view in
factsheet/views.py:1392-1394 removes only outgoing triples:
for s, p, o in oekg.triples((study_URI, OBO.BFO_0000051, None)):
oekg.remove((o, None, None))
oekg.remove((study_URI, None, None))
oekg.remove((study_URI, None, None)) matches the bundle as a subject, so the version
node — which references the bundle as an object — survives its bundle. Three orphan
triples per bundle deleted through the browser, accumulating without bound in the default
graph.
What this does not break, so the severity is judged honestly:
- A
GETof the deleted bundle still answers404. The read requires
?root a oeo:OEO_00020227, which is gone. - A
PATCHstill answers409and writes nothing. PR #2438 asserts the bundle's own type
beside the version in the guard precisely because of this — without that assertion the
guard matched a bundle that was gone and resurrected its fields as untyped orphans. The
regression test is
oekg.tests.test_bundle_patch.InterleavedWriteTest.test_a_bundle_deleted_the_way_the_interface_deletes_is_not_resurrected. - Identifiers are
uuid4and server-minted, so no future bundle can land on an orphaned
version node's IRI and inherit a stale count.
So this is accumulating garbage rather than a correctness fault — but it is garbage in the
production knowledge graph, on a path a user hits from a button, and it is worth removing
while there is little of it.
Note that this is the third defect in the same function. The other two are already
identified and separately filed from the WF-06 work: the delete destroys shared
model/framework nodes (they are minted globally as .../oekg/models/<id> and linked by the
same BFO_0000051 the delete walks, so removing one bundle strips the label from every
other bundle's copy and makes them shape-invalid), and it orphans every two-hop child.
Whoever picks up either of those should probably take this at the same time; the fix is in
the same three lines.
Ideas of solution
Three options, in rising cost.
1. Extend the UI's delete to remove the version node (recommended). The node's IRI is
derived, so no lookup is needed, and factsheet/views.py:62 already has the namespace in
scope:
oekg.remove((OEKG["version/" + id], None, None))
Cheap and obvious. Two caveats worth stating. First, factsheet/oekg/connection.py runs
with autocommit=True, so each remove is its own committed transaction and this delete is
already not atomic — one more removal does not make that worse, but it does not make it
better either. Second, spelling the IRI out here duplicates knowledge that
oekg/versioning.py owns, and the two going out of step would be silent. Prefer
from oekg.versioning import version_iri over rebuilding the string: importing the API's
versioning module is safe in this direction (it pulls in oekg.bundles and
oekg.graph_store, neither of which parses the ontology), and the reverse constraint —
that the API must not import factsheet/oekg/connection.py, which parses the full OEO at
import — is unaffected.
2. Let the API's own delete slice cover only itself, and clean up the residue once. The
API's two-step delete (slice 9 of the map, "Delete a whole bundle, in two steps") will
remove the version node with the bundle, because it is written against the same module. So
the API path will be correct without any change. That leaves the UI path leaking, plus
whatever has already leaked, which a one-off cleanup can sweep:
DELETE { ?v ?p ?o }
WHERE {
?v <https://openenergyplatform.org/ontology/oekg/versionOf> ?bundle ;
?p ?o .
FILTER NOT EXISTS { ?bundle a <https://openenergyplatform.org/ontology/oeo/OEO_00020227> }
}
Safe to run at any time and idempotent — it only removes version nodes whose bundle no
longer exists. Worth having regardless of which option is chosen, for what has already
accumulated by then.
3. Supersede the UI's delete with the API's. The right end state, and out of scope
here: the API's delete is a typed containment walk with a guard clause, so it does not have
any of these three defects. Until the UI calls it, the UI's path needs the one-line fix.
Recommendation: option 1 plus the cleanup query from option 2, folded into whichever of
the already-filed UI-delete issues is picked up first, rather than as work of its own. The
one thing not to do is nothing and not to record it, since the accumulation is silent.
Deployment
No migrations. Nothing here touches Postgres or the OEDB.
If the cleanup query is run, it is a write to the production knowledge graph and should
be treated as one:
- Count first, so the effect is known before it is applied — the same query as a
SELECT:SELECT (COUNT(DISTINCT ?v) AS ?orphans) WHERE { ?v <https://openenergyplatform.org/ontology/oekg/versionOf> ?bundle . FILTER NOT EXISTS { ?bundle a <https://openenergyplatform.org/ontology/oeo/OEO_00020227> } } - Expect a small number. Nothing accumulates before #2438 is deployed, and it grows only
by browser deletes of bundles the API has written to. - The delete is idempotent, so a re-run after a partial failure is safe.
Note that Fuseki serves queries to anyone but answers updates with 401 without valid
credentials, so the count can be taken read-only from anywhere while the delete needs the
configured RDF_DATABASE_USER / RDF_DATABASE_PASSWORD.
Workflow checklist
- I am aware of the workflow in
CONTRIBUTING.md
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.