Skip to content

Commit 2ecb50c

Browse files
authored
Merge pull request #344 from fabio-rovai/integrate/335-oxigraph-pin
The store keeps literals as written: oxigraph pin moves to 6409fa1c (#335)
2 parents ec29b8b + 4e6ba32 commit 2ecb50c

22 files changed

Lines changed: 365 additions & 217 deletions

‎CHANGELOG.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,40 @@ All notable changes to Open Ontologies are documented here.
150150
and it no longer claims that the ceiling "cannot" fire, since that was
151151
arithmetic about the tableaux and not about the run.
152152

153+
- **The store gives a literal back as it was written** (#335; the store fix
154+
is oxigraph/oxigraph#2010 by Fabio Rovai, backported onto the pinned fork
155+
as rev 6409fa1c). Oxigraph 0.5.9 stored every literal of an XSD datatype it
156+
could value-encode by value and gave it back in canonical form:
157+
`"4.0"^^xsd:decimal` came back as `"4"`, `"1"^^xsd:boolean` as `"true"`,
158+
`"5"^^xsd:int` as `"5"^^xsd:integer`, and `"4.0"` and `"4"` loaded as one
159+
triple. The pinned store value-encodes a literal only when decoding gives it
160+
back as written. So `sh:value` and a literal focus node now carry the term as
161+
written (W3C `core/node/maxExclusive-001` and `minExclusive-001` match their
162+
expected reports: report match 49 to 51 of 56; gated column 61 and full-tuple
163+
column 53 unchanged, baseline byte-identical), a load-then-serialise round
164+
trip no longer rewrites literals, and `asserted.tsv` carries literals as
165+
written. NOT fixed, because the SPARQL evaluator (`spareval`) is unpatched:
166+
inside a query `STR("4.0"^^xsd:decimal)` is still `"4"` and
167+
`DATATYPE("5"^^xsd:int)` still `xsd:integer`, so `sh:pattern "\.0$"` still
168+
reports `"4.0"^^xsd:decimal`, and `sh:datatype` naming one of the twelve
169+
integer-derived types or `xsd:dateTimeStamp` is still recorded as not
170+
evaluated (measured over every XSD datatype; pinned in
171+
`tests/datatype_preservation_test.rs` and
172+
`inside_a_query_a_known_datatype_literal_is_still_read_by_value`).
173+
174+
- **A projection-entailment goal written with a literal is asked as written**
175+
(consequence of #335). Goals are still round-tripped through a store, but the
176+
store no longer respells `"01"^^xsd:integer` as `"1"`, so a goal `"01"` asked
177+
of a source that says `"1"` is `ungrounded_in_source`, where it used to come
178+
back `preserved_asserted`. One value, two terms, which is what the checker
179+
has always done.
180+
181+
- **A `sh:datatype` skipped for an integer-derived type or `xsd:dateTimeStamp`
182+
names the real cause.** The skip reason said the store does not preserve the
183+
datatype; since #335 it does, and what cannot report it is `DATATYPE()` in the
184+
SPARQL evaluator. The reason now says so (`... so DATATYPE() cannot tell a
185+
value carrying it from one carrying that type ...`). The set of datatypes and
186+
every verdict are unchanged, by measurement on the new pin.
153187
- **A datatype the store cannot tell apart is refused inside `sh:or`,
154188
`sh:not` and `sh:qualifiedValueShape`, as it already was on its own.** The
155189
store widens xsd:int, xsd:nonNegativeInteger and the other integer-derived

‎Cargo.lock‎

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,18 +119,26 @@ toml = "0.8"
119119
# not grow by a single crate.
120120
proptest = "1"
121121

122-
# A patched Oxigraph, pinned by revision.
122+
# A patched Oxigraph, pinned by revision. Two changes over 0.5.9, on branch
123+
# `oo/lexical-forms-0.5.9` of the fork.
123124
#
124-
# `Store::snapshot` is not in Oxigraph 0.5.9. It is a read view pinned to one
125-
# state that takes no lock, and decision 0010 explains at length why neither
126-
# thing the unpatched crate offers will do: a transaction pins the state but
127-
# stops every writer for the length of a reasoning run on the memory backend,
128-
# and copying the graph costs about 221 bytes a triple. The patch is a public
129-
# wrapper over `Storage::snapshot`, which `Store::len` and
130-
# `Store::quads_for_pattern` already call internally on every read.
125+
# 1. `Store::snapshot`, not in Oxigraph 0.5.9. It is a read view pinned to one
126+
# state that takes no lock, and decision 0010 explains at length why neither
127+
# thing the unpatched crate offers will do: a transaction pins the state but
128+
# stops every writer for the length of a reasoning run on the memory backend,
129+
# and copying the graph costs about 221 bytes a triple. The patch is a public
130+
# wrapper over `Storage::snapshot`, which `Store::len` and
131+
# `Store::quads_for_pattern` already call internally on every read.
132+
# 2. A literal is kept as written (oxigraph/oxigraph#2010, fixing #1989; here
133+
# #335). 0.5.9 stored every literal of an XSD datatype with a value encoding
134+
# by value and gave it back in the canonical form of the primitive type, so
135+
# "4.0"^^xsd:decimal came back as "4", "1"^^xsd:boolean as "true",
136+
# "5"^^xsd:int as "5"^^xsd:integer, and "4.0" and "4" loaded as one triple.
137+
# A literal is now value-encoded only when decoding gives back the same
138+
# datatype and lexical form; any other takes the generic typed-literal path.
131139
#
132140
# Pinned by REVISION and not by branch, so that what CI builds is what was
133-
# reviewed. Upstream at oxigraph/oxigraph; when it lands this stanza goes and
141+
# reviewed. Upstream at oxigraph/oxigraph; when both land this stanza goes and
134142
# the version bound below is all that remains.
135143
[patch.crates-io]
136-
oxigraph = { git = "https://github.com/fabio-rovai/oxigraph.git", rev = "abf36ef22b61f9c4d810b94b647d500c05ee791c" }
144+
oxigraph = { git = "https://github.com/fabio-rovai/oxigraph.git", rev = "6409fa1c8ad5880399d0ca69b38e43cbb2052ad7" }

‎docs/UPSTREAM_ISSUES.md‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ send.
88

99
## 1. Twelve XSD integer-derived datatypes, and `xsd:dateTimeStamp`, are not preserved
1010

11+
**Status, 28 Oct 2026.** 1a is fixed in the pinned fork and filed upstream as
12+
oxigraph/oxigraph#2010 (issue #1989): the store value-encodes a literal only
13+
when decoding gives it back as written, so these datatypes, and non-canonical
14+
lexical forms such as `"4.0"^^xsd:decimal`, now survive a load (here #335,
15+
`tests/datatype_preservation_test.rs::the_store_keeps_every_declared_datatype`).
16+
1b is NOT fixed: measured on the new pin, `DATATYPE()` still answers
17+
`xsd:integer` for the twelve and `xsd:dateTime` for `xsd:dateTimeStamp`, and
18+
`STR()` of a known-datatype literal is still its canonical form, inside any
19+
query, whether the literal is stored or written in the query text. So the
20+
workaround below stays.
21+
1122
**Two independent defects, in two crates.** They were first reported here as one,
1223
blamed on storage. That was wrong, and the correction matters for anyone acting
1324
on it: fixing the storage encoder alone leaves `DATATYPE()` still returning the
@@ -113,9 +124,11 @@ up here rather than attempted blind.
113124
### Our workaround
114125

115126
`datatype_is_indistinguishable_in_store` in `src/shacl.rs` records a
116-
`sh:datatype` constraint naming one of these as unevaluated rather than answering
117-
it wrongly. `tests/datatype_preservation_test.rs` pins the exact affected set and
118-
fails in both directions, so the workaround is removed when this is fixed.
127+
`sh:datatype` constraint (and, since the #333 follow-ups, an `sh:or`, `sh:not`,
128+
qualified or `sh:node` member) naming one of these as unevaluated rather than
129+
answering it wrongly. With 1a fixed it rests on 1b alone.
130+
`tests/datatype_preservation_test.rs` pins the exact set `DATATYPE()` misreports
131+
and fails in both directions, so the workaround is removed when 1b is fixed.
119132

120133
---
121134

‎docs/decisions/0007-a-slice-preserves-a-conclusion-or-it-does-not.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ test fails if the gate is disarmed on more than half the corpus.
215215
certificate and is reported LOST, which is a false loss with a green-looking pipeline round it. Goals are
216216
now round-tripped through a real `GraphStore`, one triple per named graph so a set cannot collapse
217217
duplicates or shift the caller's pairing, and both `as_written` and `goal` are printed.
218+
*Superseded in part by #335 (28 Oct 2026):* the pinned store no longer canonicalises, so the
219+
round trip leaves `"01"` as `"01"`, and a goal `"01"` asked of a source holding `"1"` is
220+
ungrounded rather than preserved. The round trip stays so that goal and store are spelled by the
221+
same code.
218222

219223
4. **`GraphStore::canonicalize_blank_nodes` is the wrong tool and is now pinned as such.** RDFC-1.0
220224
labels are a function of the WHOLE graph. Measured on `pizza-reference.owl`, taking a genuine

‎docs/decisions/0018-conformance-is-not-truth.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,11 @@ line between the buckets was drawn.
204204

205205
## The control, and the bound it actually has
206206

207-
`"300"^^xsd:byte` is outside the byte range and is the W3C suite's own ill-formed case. Oxigraph does
208-
not preserve `xsd:byte`, so the literal arrives as `"300"^^xsd:integer`,
209-
`datatype_is_indistinguishable_in_store` routes the constraint to `skipped_constraints`, and the
210-
verdict is `null`. An attack surface where every probe succeeds is not a measurement, so this one is
207+
`"300"^^xsd:byte` is outside the byte range and is the W3C suite's own ill-formed case. Oxigraph
208+
0.5.9 did not preserve `xsd:byte`, so the literal arrived as `"300"^^xsd:integer`. Since #335 the
209+
store keeps it as written, but the SPARQL evaluator still reads `xsd:byte` as `xsd:integer`, so
210+
`DATATYPE()` cannot judge it, `datatype_is_indistinguishable_in_store` routes the constraint to
211+
`skipped_constraints`, and the verdict is `null`. An attack surface where every probe succeeds is not a measurement, so this one is
211212
kept beside the failures rather than in a passing-cases file.
212213

213214
The bound is one-directional and the test now says so. A consumer reading for a PASS is protected,

‎docs/lean-certificates.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -922,8 +922,9 @@ Stated rather than discovered later.
922922
exactly their own rule once. No rule in the table is unreachable, so the seven were a gap in the
923923
corpus and not a defect in the engine.
924924
- **`asserted.tsv` is the store, not your file.** Quads are flattened, so a triple present in two
925-
named graphs appears on two lines. Literals are in the store's post-parse canonical spelling, so
926-
`"01"^^xsd:integer` is written `"1"^^xsd:integer`. The guarantee is relative to that file.
925+
named graphs appears on two lines. Literals are as the store gives them back, which since #335
926+
is as they were written: `"01"^^xsd:integer` stays `"01"` (under Oxigraph 0.5.9 it was written
927+
`"1"^^xsd:integer`). The guarantee is relative to that file.
927928
- **`asserted.tsv` is the store the run SELECTED, and the checker cannot see the selection.** By
928929
default that is every graph. Over a store that keeps several versions of an entity in several
929930
named graphs, the union of every version is a state that held at no instant, and a certificate

‎docs/projection-entailment.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ EOF
4848
```
4949

5050
`--goals` is a Turtle document in which every triple is a claim. It is parsed by the SAME parser the
51-
store uses and round-tripped through a store, which is the only thing that makes a caller's spelling
52-
of a term comparable with the interner's: `"01"^^xsd:integer` is stored as `"1"^^xsd:integer`, and a
53-
goal that skipped the store would match nothing and be reported lost. A TSV works too, with
51+
store uses and round-tripped through a store, so a caller's spelling of a term is the interner's. A
52+
literal is asked as written: since #335 the store keeps `"01"^^xsd:integer` as `"01"`, so the goal
53+
matches a source that says `"01"` and not one that says `"1"`. One value, two terms; nothing here
54+
compares literals by value. (Under Oxigraph 0.5.9 the store respelled both as `"1"`, and the two
55+
matched.) A TSV works too, with
5456
`--goals-skip-columns 1` so `derivations.tsv` from `reason --certificate` pipes straight in.
5557

5658
Over MCP, `graph_projection_entailment_check` takes `goals_ttl` and either `projected_ttl` or

‎src/graph.rs‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,13 +1392,14 @@ impl GraphStore {
13921392
/// Round-trip triples through a STORE, in order, keeping duplicates.
13931393
///
13941394
/// Parsing alone is not enough, and this is measured rather than assumed:
1395-
/// oxigraph's parser preserves a literal's lexical form, and it is the
1396-
/// STORE that normalises it. `"01"^^xsd:integer` comes back from
1397-
/// [`parse_triples_ordered`](Self::parse_triples_ordered) exactly as
1398-
/// written and out of [`all_triples`](Self::all_triples) as
1399-
/// `"1"^^xsd:integer`. A caller that compares its own spelling of a term
1400-
/// against what a certificate holds therefore has to push the term through
1401-
/// a store first, or it matches nothing and the miss looks like data loss.
1395+
/// A caller that compares its own spelling of a term against what a
1396+
/// certificate holds pushes the term through a store first, so the two are
1397+
/// spelled by the same code. Until #335 that was what made them match:
1398+
/// Oxigraph 0.5.9's parser kept `"01"^^xsd:integer` and its store gave it
1399+
/// back as `"1"^^xsd:integer`. The pinned store keeps literals as written,
1400+
/// so the round trip no longer respells a literal, and `"01"` and `"1"`
1401+
/// are two terms on both sides; it still refuses what no serialiser can
1402+
/// write, and it keeps the comparison honest if the store changes again.
14021403
///
14031404
/// Each triple gets its own named graph, because a store is a SET: two
14041405
/// identical inputs would otherwise collapse to one and the caller's

‎src/ofn_writer.rs‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,13 @@ pub fn is_ofn_format(name: &str) -> bool {
231231
/// cannot disagree about what was left out.
232232
///
233233
/// The document's triples are handed over as parsed, without passing through
234-
/// a store: Oxigraph's store re-types an `xsd:nonNegativeInteger` literal as
235-
/// `xsd:integer` (measured on `owl:minCardinality 2`), and every cardinality
236-
/// that had happened to would be refused as outside Table 1.
234+
/// a store, so nothing between the file and the writer can respell a term.
235+
/// That was first done because Oxigraph 0.5.9's store re-typed an
236+
/// `xsd:nonNegativeInteger` literal as `xsd:integer` (measured on
237+
/// `owl:minCardinality 2`), and every such cardinality would have been refused
238+
/// as outside Table 1. The pinned store keeps literals as written since #335
239+
/// (`tests/datatype_preservation_test.rs`); reading without one is kept
240+
/// because it is the shorter path, not because a store would now be wrong.
237241
pub fn convert_file(path: &str) -> anyhow::Result<OfnOutput> {
238242
let (quads, prefixes) = crate::graph::GraphStore::read_file_with_prefixes(path)?;
239243
let triples: Vec<Triple> = quads.into_iter().map(Triple::from).collect();

0 commit comments

Comments
 (0)