Skip to content

deterministic_turtle is not idempotent on graphs containing shared RDF collections #1

Description

@jdsika

deterministic_turtle is not idempotent on at least one real graph: canonicalizing its own
output produces a different file, and the process does not converge.

Symptom

pass 0 (as written)  sha=8a4bcbd8c0de4252  229326 bytes  5115 triples
pass 1               sha=f1eddc962da07919  230308 bytes  5267 triples
pass 2               sha=97f31502b3284172  231294 bytes  5419 triples
pass 3               sha=cecca06d8634020e  232274 bytes  5571 triples
pass 4               sha=4ba887b9604efdd2  233254 bytes  5723 triples

+152 triples per pass, indefinitely. Comparing predicate histograms between passes, only
collection plumbing changes
:

predicate pass 1 pass 2
rdf:first 352 428
rdf:rest 352 428

Every other predicate is unchanged, so no statement is gained or lost — the collections are
being duplicated on each round trip.

Why it matters

This breaks the property the library exists to provide. A consumer that canonicalizes generated
RDF and commits it cannot verify the result: a CI check of the form "re-canonicalize and compare
bytes" fails on a file that was just canonicalized. That is exactly how we use it — see
ASCS-eV/asam-openx-standards, scripts/check_canonical.py.

Reproduction

The graph is a SHACL shapes file produced by shacl-play owl2shacl, attached below. It contains
81 sh:in constraints referencing 51 distinct collection nodes — 16 of those are referenced by
more than one property shape, with up to 5 references each.

from rdflib import Graph
from diffable_rdf import deterministic_turtle

cur = open("repro-shacl.ttl", "rb").read()
for i in range(5):
    g = Graph().parse(data=cur, format="turtle")
    print(i, len(g))
    nxt = deterministic_turtle(g).encode()
    if nxt == cur:
        print("fixed point")
        break
    cur = nxt

What I could not narrow down

Shared collections alone are not sufficient to trigger it. One collection node shared by
two, three, four or five subjects converges after a single pass:

N=2: converges  triples per pass: [6, 6]
N=3: converges  triples per pass: [7, 7]
N=4: converges  triples per pass: [8, 8]
N=5: converges  triples per pass: [9, 9]

So does the case of several subjects each owning an identical-content collection. The trigger is
therefore narrower than plain sharing — plausibly an interaction between the
Weisfeiler-Lehman blank-node hashing and collections whose cells are structurally
indistinguishable, but I did not isolate it. The attached file reproduces it reliably.

Environment

diffable-rdf 0.0.1, rdflib 7.6.0, pyoxigraph 0.5.9, Python 3.12.

Suggestion

Whatever the cause, a regression test asserting deterministic_turtle reaches a fixed point in
one pass would be worth having for any graph containing rdf:Lists — that is the invariant the
library promises and the one that broke.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions