Skip to content

fix(turtle): resolve RFC 3986 §5.3 fragment refs against @base - #1399

Merged
aaj3f merged 1 commit into
mainfrom
fix/turtle-fragment-resolution
Jun 30, 2026
Merged

aaj3f merged 1 commit into
mainfrom
fix/turtle-fragment-resolution

Conversation

@aaj3f

@aaj3f aaj3f commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

fix(turtle): resolve RFC 3986 §5.3 fragment refs against @base

Summary

The Turtle parser's resolve_iri dropped the fragment when resolving a
relative IRI reference such as <#Name> against an @base. As a result, every
<#DimDate>, <#DimProduct>, … collapsed to the same IRI (the base with its
fragment stripped). This is a direct RFC 3986 §5.2.2 / §5.3 violation and is the
root cause of issue #1395: an idiomatic @base + <#Name> multi-TriplesMap
R2RML mapping silently collapsed into a single TriplesMap / single logical table.

This PR is Phase 1 — the foundational parser fix at the bottom of the
fluree/db ↔ Snowflake-managed-Iceberg stack. It is fully self-contained in the
fluree-graph-turtle crate.

Root cause

Per RFC 3986 §5.2.2, the fragment of a resolved reference is always the
reference's own fragment (T.fragment = R.fragment); it is never inherited from
the base. The previous resolve_iri (a) returned None for the fragment on the
#-only branch, (b) never appended #<fragment> during component recomposition,
and (c) used helpers (split_path_query) that strip everything after #. So the
reference fragment was discarded on every relative resolution, and the empty <>
reference incorrectly returned the base with its fragment intact. Existing
R2RML fixtures all used absolute TriplesMap IRIs, which short-circuit
resolution entirely, so the bug was never exercised.

What changed

fluree-graph-turtle/src/parser.rs — resolve_iri only:

  • Split the reference into its fragment (everything after the first #) and the
    fragment-less remainder up front (RFC 3986 §5.2.1). Scheme/authority/path/query
    are resolved against the remainder; the reference fragment is re-attached during
    recomposition (§5.3).
  • Same-document references (<> and <#frag>) now inherit the base path and
    query
    but not the base fragment (the base fragment is naturally dropped by
    parse_iri_components).
  • Append #<fragment> in the result assembly when (and only when) the reference
    itself carries a fragment.
  • Removed the special-case early return for the empty <> reference, which had
    returned the full base including its fragment; <> now correctly resolves to
    base-minus-fragment.

No helper signatures changed and no other call sites were touched. Absolute-IRI
and prefixed-name inputs are completely unaffected.

Tests added

Six new parser unit tests in fluree-graph-turtle/src/parser.rs (all driven
through the real parse path via parse_to_graph):

  • test_base_fragment_refs_resolve_to_distinct_iris — the regression guard for
    Turtle parser drops the fragment resolving <#name> against @base, collapsing multi-TriplesMap R2RML mappings to one table #1395
    : with @base <http://example.org/edw>, <#A> and <#B> resolve to two
    DISTINCT IRIs (…/edw#A, …/edw#B).
  • test_base_relative_path_with_fragment — <DimDate#col> → …/DimDate#col.
  • test_fragment_ref_against_base_with_fragment — a base that itself has a
    fragment: <#new> replaces it; <other#f> carries its own fragment.
  • test_empty_ref_against_fragmented_base_drops_fragment — <> against a
    fragmented base → base minus fragment.
  • test_ref_without_fragment_does_not_inherit_base_fragment — a ref with no
    fragment does NOT inherit the base's.
  • test_query_and_fragment_resolution_rfc3986 — RFC 3986 §5.4.1 query+fragment
    examples against @base <http://a/b/c/d;p?q> (<#s>, <?y#s>, <g#s>,
    <g?y#s>, <>).

Results

  • cargo test -p fluree-graph-turtle — 77 passed, 0 failed (incl. the 6 new + 1 doctest).
  • cargo clippy -p fluree-graph-turtle --all-targets — clean, no warnings.
  • cargo fmt -p fluree-graph-turtle — applied.
  • Downstream: cargo test -p fluree-db-r2rml — 46 passed, 0 failed;
    cargo check -p fluree-db-api — clean.

Risk / compatibility

resolve_iri is the resolution path for all relative IRIs in Turtle ingestion
(R2RML loader, fluree-db-memory/turtle_io.rs). Fragments were always dropped
before, so any @base + fragment Turtle was already parsed incorrectly — the fix
is strictly more correct. The change is conservative:

  • No fixtures or golden snapshots encode the old fragment-dropped behavior. The
    only .ttl carrying @base is the MCP server's own .fluree-memory/repo.ttl
    (not a test fixture). The W3C SPARQL testsuite uses its own separate base
    resolver (testsuite-sparql/src/result_format.rs), not this resolve_iri.
  • The one existing empty-ref test (test_empty_iri_resolves_to_base) uses a base
    with no fragment and stays green.
  • Absolute-IRI and prefixed-name inputs are unchanged.

Phases 2–4 from issue #1395 (R2RML dup-IRI hardening, Tables: N UX, O(1) map
selection) are intentionally out of scope for this PR and stack on top.

Fixes #1395

resolve_iri dropped the fragment when resolving a relative reference such
as <#Name> against an @base, collapsing every <#A>/<#B> to the same base
IRI (base minus its fragment). Per RFC 3986 §5.2.2 the resolved fragment
is always the reference's own fragment and is never inherited from the
base. Split the reference fragment off up front, resolve scheme/authority/
path/query against the remainder, and re-attach #<fragment> during §5.3
recomposition. Same-document refs (<> and <#frag>) now keep the base path
and query but drop the base fragment; <> resolves to base-minus-fragment.

Adds parser unit tests including the regression guard that <#A> and <#B>
resolve to distinct IRIs, plus RFC 3986 §5.4.1 query+fragment cases.

Refs #1395
@aaj3f
aaj3f requested review from bplatz and zonotope June 30, 2026 18:52

@bplatz bplatz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

@aaj3f
aaj3f merged commit 5a176d8 into main Jun 30, 2026
13 checks passed
@aaj3f
aaj3f deleted the fix/turtle-fragment-resolution branch June 30, 2026 20:14
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.

Turtle parser drops the fragment resolving <#name> against @base, collapsing multi-TriplesMap R2RML mappings to one table

2 participants