Skip to content

chore: feature integration branch for the ENVITED-X pipeline - #3

Draft
jdsika wants to merge 5 commits into
mainfrom
feat/envited-x-pipeline
Draft

jdsika wants to merge 5 commits into
mainfrom
feat/envited-x-pipeline

Conversation

@jdsika

@jdsika jdsika commented Sep 14, 2026 •

Copy link
Copy Markdown

Integration branch. Tracking only, do not merge.

What this branch is

feat/envited-x-pipeline is upstream RDFLib/rdflib@main with every contribution this
organisation is still carrying cherry-picked on top, and nothing else. It is the branch to
check out when you need an rdflib that behaves the way our pipeline expects before
upstream has merged the relevant PRs.

It is rebased on upstream main, never merged into. An empty cherry-pick list is the goal
state: it would mean upstream has merged everything, and the branch could be deleted.

Same convention as ASCS-eV/linkml#14.

What it currently carries

Commit Upstream PR Fork mirror Change
c565778a RDFLib/rdflib#3543 #2 JSON-LD collection syntax can silently duplicate shared RDF lists
31caa09d RDFLib/rdflib#3546 #1 Deterministic N-Triples serialization via an opt-in canon parameter
b3021e5b RDFLib/rdflib#3564 #4 SPARQL IN / NOT IN must use value equality, not term equality

All three cherry-pick cleanly onto upstream main at 9099a782 with no conflicts. Each
commit is patch-identical to its upstream PR head.

Last verified on this branch, Python 3.12: black, ruff and mypy are clean for the whole
project, and the full test suite gives 7754 passed, 1807 skipped, 327 xfailed, 36 xpassed, no failures. CI: Validate run, started by hand: push and pull-request events do not start workflows in this fork.

What consumes it

Nothing yet, deliberately. asam-openx-standards pins released rdflib==7.6.0
through scripts/requirements.txt, and diffable-rdf depends on released rdflib too.

That pin is the point: rdflib performs the final serialization of every generated OWL and
SHACL artifact, so a change here moves artifact bytes. Consuming this branch would mean
building from a fork, which the pipeline's reproducibility rules do not allow: the lock
records released versions precisely so a third party can obtain them.

The relationship is therefore one-directional. What lands upstream here eventually arrives
as a released rdflib that the pipeline can bump to, with regenerated artifacts in the same
change. The merged #3504 (the Turtle variant
of the RDFLib#3543 defect) is the precedent: diffable-rdf had to work around it until it was
released.

When a release contains RDFLib#3543, diffable-rdf's sentinel test
test_rdflib_jsonld_serializer_does_not_preserve_shared_list_tail will fail by design;
that is the signal to retire the workaround.

multimeric and others added 2 commits October 13, 2026 13:37
* Convert Define into an enum

* Comment explaining design

* Make Undef falsy

* Re-run CI

* Add tests for UNDEF
SUBSTR converted the 1-based start to a Python slice index without clamping,
so a start of 0 or below became a negative index counted from the end.
SUBSTR("foobar", 0) returned "r" and SUBSTR("foobar", 0, 2) returned "",
where the SPARQL and XPath definition gives "foobar" and "f".
@jdsika
jdsika force-pushed the feat/envited-x-pipeline branch from 4bf22dd to 980b7d6 Compare October 14, 2026 12:14
jdsika and others added 3 commits October 14, 2026 14:53
Converter.to_collection walked an rdf:List chain checking only that each cell
carries one rdf:first/rdf:rest pair and that the chain is acyclic. It did not
check how many statements point into the chain.

The @list form inlines the whole chain at a single reference and writes its
cells nowhere else, so when a cell is referenced from more than one place each
referring list rendered the shared cells again. The round trip then gained
triples:

    _:tail rdf:first "b" ; rdf:rest rdf:nil .
    _:head rdf:first "a" ; rdf:rest _:tail .
    ex:s1  ex:p _:head .
    ex:s2  ex:p _:tail .            # second reference into the chain

serialized as two independent @list values, so the tail was emitted twice:
6 triples in, 8 out, not isomorphic. A head referenced twice was duplicated
the same way: 4 in, 6 out.

The Serialize RDF as JSON-LD Algorithm (JSON-LD 1.1 Processing Algorithms and
API, section 8.4) only turns a chain into @list when every cell, the head
included, is a blank node that is "referenced once", counted over the whole
dataset. The converter now applies that condition. It counts blank-node
references once per conversion, over every graph it writes, and a chain with
a cell referenced more than once is written with explicit rdf:first/rdf:rest,
which can express any arrangement of sharing. Counting across graphs also
covers a cell that another named graph points at, whose reference was
otherwise left dangling.

add_to_node offered a term declared with "@container": "@list" for any object
with an rdf:first, then iterated to_collection without checking for None. That
already raised TypeError for a chain the converter refuses, such as one whose
cell carries an extra property, and would have for every shared chain. The
term is now chosen from to_collection's result instead.

This is the defect RDFLib#3504 fixed in the Turtle and longturtle serializers; the
JSON-LD converter has its own chain walk.

Fixes RDFLib#3542.
…eter

The N-Triples serializer writes statements in the order the store iterates
them, which follows set iteration and so varies between processes. The same
graph therefore serializes to different bytes in different runs, which shows
up as diff noise in version-controlled RDF.

RDFLib#3008 solved this for longturtle (RDFLib#1890) by canonicalizing with RGDA1 and
sorting N-Triples rows, and RDFLib#3197 put that behind an opt-in `canon` parameter
because of its cost (RDFLib#3196). This gives NTSerializer the same parameter, with
the same meaning, the same canonicalization and the same default of False, so
ordinary serialization is untouched and still streams statement by statement.

`canon=True` canonicalizes with rdflib.compare.to_canonical_graph and writes
the rendered rows in sorted order. Sorting the rendered rows rather than the
triples keeps the order total: two distinct terms can share a str(), which
would leave their relative order down to the store iteration order this is
meant to remove, whereas identical rows mean identical statements.

For a Dataset or ConjunctiveGraph it canonicalizes the statements the default
path writes, the triples of every graph; a Dataset iterates quads, which the
canonicalizer does not accept. The docstring states that this is not RDFC-1.0,
that the labels may change between rdflib versions, and what it costs.

Serializing one graph under six PYTHONHASHSEED values gives six distinct
documents by default and one with canon=True. NT11Serializer inherits the
parameter by subclassing.
RelationalExpression evaluated IN / NOT IN membership with Python `==`, which
is strict RDF term equality, while `=` and `!=` use Identifier.eq, which is
value equality. SPARQL 1.1 17.4.1.9 defines IN as exactly

    (lhs = expression1) || (lhs = expression2) || ...

("The test is done with "=" operator, which tests for the same value"), and
17.4.1.10 defines NOT IN as its negation, so the two must never disagree.

They did. RDF 1.1 treats a simple literal and an xsd:string-typed literal as
the same term, and XSD type promotion makes 1 and 1.0 equal in value space,
so these evaluated incorrectly while the equivalent `=` comparisons were
correct:

    "x"^^xsd:string IN ("x")                  false, should be true
    1 IN (1.0)                                false, should be true
    2 IN (<http://example/iri>, "str", 2.0)   false, spec example says true

FILTER(?v NOT IN (...)) therefore admitted values that are in the list. It was
found through a SHACL sh:sparql constraint whose data reached the query as
xsd:string-typed literals while the shape listed plain ones.

Membership now uses Identifier.eq, with the error handling `=` already has: a
NotImplemented result (truthy, and a TypeError in a boolean context on Python
3.14) and a comparison TypeError are recorded as errors. Errors follow
17.4.1.9, "Errors in comparisons cause the IN expression to raise an error if
the RDF term being tested is not found elsewhere in the list", and now do so
uniformly: an erroring list entry is an error for an IRI on the left as well
as for a literal, and an erroring left-hand side is an error rather than a
result.

    2 NOT IN (1/0)          true -> error
    <urn:x> NOT IN (1/0)    true -> error
    (1/0) NOT IN (2)        true -> error

With rdflib.DAWG_LITERAL_COLLATION set, `=` raises a type error for literals
that share no value space (RDFterm-equal, 17.4.1.7), and IN now does too.

The W3C SPARQL 1.0 and 1.1 test suites give the same results before and
after.
@jdsika
jdsika force-pushed the feat/envited-x-pipeline branch from 980b7d6 to b3021e5 Compare October 14, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants