feat(schemaview): TermDescriptor and RangeInfo::is_numeric() - #113
Merged
Conversation
Answers the question a consumer asks when deciding whether a column's values compare as numbers or as text: '9' >= '10' holds as text and fails as a number, so getting it wrong is silent in both directions. is_integer/is_floating_point cannot answer it. They were written to pick a JSON canonicalisation at boxing time, and between them they cover exactly integer, float, double and decimal -- so a slot declared xsd:int, xsd:long, xsd:short, xsd:byte, any xsd:unsigned*, or any of the bounded-integer datatypes reads as non-numeric. Those four names stay as they are: their callers want the narrower question. is_numeric uses the same IRI-primary, builtin-name-fallback resolution: the resolved datatype IRI also catches schema-defined subtypes, and the builtin LinkML type names keep detection working against a schema whose linkml:types is not loaded -- which is the case a consumer keeping its own list of datatype IRIs gets wrong. LinkML has no builtin name for the XSD numeric datatypes beyond those four, so a schema can only reach them by declaring its own type; the test fixture does that once per datatype and asserts detection comes off the IRI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The turtle writer spelled out the same precedence chain at three of its call sites (single value, list item, mapping value): reference-or-IRI-range, then enum meaning, then literal. Each copy was ~20 lines of triple building, and the chain's ordering invariant was nowhere stated. Lift the decision to SlotView::term_descriptor(), which resolves it from the slot alone -- an enum meaning map, an IRI disposition, a datatype, a language tag -- and returns None when the values are not a term anything can reproduce (an inlined structure serializes as a blank node whose label nothing can reproduce). turtle::term_for() applies a descriptor to one value, so the three call sites collapse to one line each and turtle.rs loses 41 lines net. Writing the chain once also makes an invariant visible that was previously accidental: the writer tested is_range_iri before the enum meaning while the order is in fact immaterial, because determine_rdf_type_info yields (None, false) for an enum range and so is_range_iri is never true for one. The split across crates is deliberate. The descriptor is schema knowledge and lives in schemaview; term_for stays in the runtime because schemaview depends on neither oxrdf nor serde_json and should not start. Consumers that must render values without serializing them -- pushing a query down to SQL over stored JSON, where the rendering is decided at plan time and has to match this writer term for term -- need the descriptor, not the Term. term_descriptor takes the converter rather than deriving one, so the enum meaning CURIEs expand exactly as they do during serialization; deriving one internally would diverge for a schema with conflicting prefixes. Tests: one case per precedence rule on the descriptor (schemaview), the exact Term per rule through term_for (runtime, on the same fixtures the golden turtle tests use, including the mixed enum whose `unknown` value has no meaning and a CURIE-valued IRI range), and the existing turtle serialisation tests passing unchanged -- which is what makes this a refactor rather than a rewrite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two additions requested by a downstream consumer that currently reimplements this logic locally
because it is private here. Both are things this crate wants anyway.
1.
TermDescriptor— the value→term decision, without a valueThe turtle writer spelled out the same precedence chain at three call sites (single value, list
item, mapping value): reference-or-IRI-range → enum meaning → literal. Each copy was ~20 lines of
triple building, and the chain's ordering invariant was stated nowhere.
SlotView::term_descriptor(&Converter)resolves that chain from the slot alone — an enum meaningmap, an IRI disposition, a datatype, a language tag — and returns
Nonewhen the values are not aterm anything can reproduce (an inlined structure serializes as a blank node whose label nothing
can reproduce).
turtle::term_for(&descriptor, value, conv)applies one to a single value.The three call sites collapse to one line each;
turtle.rsis 41 lines shorter.Writing the chain once also makes an invariant visible that was previously accidental: the writer
tested
is_range_iribefore the enum meaning, while the order is in fact immaterial, becausedetermine_rdf_type_infoyields(None, false)for an enum range sois_range_iriis never truefor one.
Two deliberate deviations from the shape that was requested:
term_foris a runtime function, not aTermDescriptormethod.schemaviewdepends onneither
oxrdfnorserde_jsonand shouldn't start; the descriptor is schema knowledge, theTermis not.term_descriptortakes the converter rather than deriving one internally, so enummeaningCURIEs expand exactly as they do during serialization. Deriving one would diverge for a schema
with conflicting prefixes — which this repo has a test for.
2.
RangeInfo::is_numeric()The question a consumer asks when deciding whether a column's values compare as numbers or as
text:
'9' >= '10'holds as text and fails as a number, so getting it wrong is silent in bothdirections.
is_integer/is_floating_pointcannot answer it. They pick a JSON canonicalisation at boxingtime, and between them cover exactly
integer,float,double,decimal— so a slot declaredxsd:int,xsd:long,xsd:short,xsd:byte, anyxsd:unsigned*or any bounded-integer datatypereads as non-numeric. Both are left exactly as they are; their callers want the narrower question.
is_numericuses the same IRI-primary, builtin-name-fallback resolution, so it also catchesschema-defined subtypes and still works against a schema whose
linkml:typesis not loaded.Testing
schemaview/tests/term_descriptor.rs),including the enum with no
meaning(must stay a literal) and a slot declaring bothin_languageand a datatype (the datatype must win).Termper rule throughterm_for(
runtime/tests/term_descriptor_parity.rs), on the same fixtures the golden turtle tests use —including the mixed enum whose
unknownvalue has no meaning, and a CURIE-valued IRI range.refactor rather than a rewrite.
is_numeric, plus the negatives (string, boolean, date,uriorcurie, a class range, an enum range) and a schema with notypes:block at all, which isthe case a hand-kept list of datatype IRIs gets wrong.
cargo fmt --all --check,cargo clippy ... -D warnings,cargo test --workspace(73 suites) andthe stub_gen check are all clean.
Not in this PR
turtle.rsstill has twoliteral_and_typecall sites (~lines 555, 611) that make adatatype-only version of the same decision for top-level collections of bare scalars emitting
rdf:value. Routing those through the descriptor would change serialisation output (they wouldstart honouring language tags, enum meanings and IRI ranges), so it wants its own change.
try_lang_tag_collapse— thejsonld:language/jsonld:valueclass pattern — still collapses aninlined object into a language-tagged literal outside the descriptor. The descriptor returns
Nonefor inlined ranges, so a consumer refuses those columns rather than disagreeing.