fix(query): cross-ledger explicit projection drops foreign-namespace predicates - #1417
Merged
Merged
Conversation
A cross-ledger JSON-LD explicit projection silently dropped predicates whose namespace code differs between the primary (lowering) ledger and the ledger that stores the subject. The projection spec is lowered once against the primary ledger's namespace dict, so its predicate Sids carry the primary's codes; when hydration crossed into another ledger they missed the target's SPOT index and select_predicate, so foreign-namespace predicates were elided (reserved namespaces survived, their codes stable). expand_ref now re-encodes the immediate level's predicate Sids into the target ledger's dict (rebind_level_to_view), memoized per (target, level) via a bundled HydrationCaches so each rebind runs once per response and reuses a shared Arc. Single-ledger and same-ledger refs are untouched. Adds cross-ledger repro + wildcard-refinement coverage; the non-primary hydration-root case (@id-only) is a separate root-routing bug, left as an ignored repro pending follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1295) The nested cross-ledger fix re-encodes projection predicate Sids at each `expand_ref` crossing, but a hydration *root* whose subject lives in a non-primary ledger takes a different path: `format_hydration_column` routes the root to its home-ledger formatter (via `FormatterSet::pick`) yet still passes `spec.level` with predicate Sids lowered against the primary dict. Divergent-namespace predicates then miss the routed view's index and are silently dropped, while reserved/shared-code predicates survive (their codes are stable across ledgers) — masking the loss. Rebind the projection level into the routed view's namespace dict when the root is non-primary, reusing the `rebind_level_to_view` primitive and the shared per-response memo. Roots that stay on the primary view are byte- identical (no allocation). Single-ledger hydration is unaffected: its FormatterSet holds only the primary formatter, so the fast path always fires. Adds `cross_graph_root_projection_divergent_predicate` (red→green): a root bound in its home graph hydrates `schema:name` (shared code) but dropped `p:fullName` pre-fix. Distinct from the still-deferred Finding B, where the subject comes back `@id`-only because routing itself fails. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hydrator's cross-ledger namespace translation was named "rebind" (`rebind_level_to_view`, `rebinds`, `rebound_arc`) while its own prose and the sibling WHERE-scan subsystem both call the identical operation "re-encode" (`fluree_db_query::binary_scan::reencode_sid`). "rebind" also collides with the codebase's established "binding" vocabulary (variable bindings, `IriMatch` provenance), inviting misreading. Rename to the precise, already-established term: rebind_level_to_view -> reencode_level_for_view rebind / rebind_map -> reencode / reencode_map HydrationCaches.rebinds -> reencoded_levels rebound_arc -> reencoded_arc Pure rename — no behavior change. Adds reciprocal cross-reference comments between the hydration re-encode and the scan re-encode noting they are the same primary->target operation with deliberately opposite miss policies (scan preserves the raw Sid; hydration drops the predicate). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ience The #1295 comments over-explained mechanics a core engineer already knows (namespace codes, ledger-local Sids, what lowering does). Trim to the load-bearing content — the shallow-re-encode invariant (decode source is always the primary/lowering view, even in depth-N chains), the drop-vs-preserve miss policy, and the scan<->hydration cross-references — and drop the tutorial. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The inline comment on `let mut cache` described only the `results` memo (and still called the key's leading field the "active ledger", stale since the active-view-index switch in #1259). Now that `cache` is the bundled `HydrationCaches`, defer to that struct's doc — the single source of truth for the key shape and both memos — instead of restating (and re-drifting from) it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "NOTE (staged)" claimed nested cross-ledger refs still expand within the root's view and are correct only when the two ledgers allocated matching namespace codes — exactly the divergence #1295 fixed. Replace it with the current behavior (nested + root projection predicates re-encode per crossing) and the one honest remaining gap: a root with no home-ledger provenance (bound as the object of a primary-ledger triple) can still come back @id-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A subject IRI described in BOTH default-graph ledgers with the same
multi-cardinality predicate (skos:altLabel, different values each) should render
as the union of both under default-graph RDF merge semantics.
- cross_graph_nested_ref_unions_split_subject_altlabels (passing): the nested
`expand_ref` path merges {WA, WB} across the default-graph union.
- cross_graph_root_unions_split_subject_altlabels (#[ignore]): the SAME split
subject bound at the root returns only its provenance ledger's value {WA} —
`format_hydration_column` / `FormatterSet::pick` route to one home ledger and
never union, unlike `expand_ref`. Companion to Finding B; both are root-path
limitations pending a follow-up that unifies the root with expand_ref's
union+merge.
The root is bound via a plain predicate (ex:kind), not @type, so the repro
exercises only root-hydration routing and not any @type/rdf:type value handling.
Named-graph queries are out of scope by design (a named graph is addressed
individually; no cross-graph merge expected).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the reciprocal note added to binary_scan.rs's reencode_sid. It was net-zero informative but pulled fluree-db-query into the PR's changed files; the one-directional pointer from hydration.rs's reencode_level_for_view is enough. The PR now touches a single source file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The new test helpers tripped rustfmt (multi-line match arm + chained Option calls), failing both the fmt job and testsuite-sparql's Format gate. No logic change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Jackamus29
force-pushed
the
fix/1295-cross-ledger-projection-rebind
branch
from
July 2, 2026 02:59
d64894e to
e2196e0
Compare
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.
Closes #1295.
Problem
A cross-ledger JSON-LD explicit projection over a multi-ledger
fromsilently drops any predicate whose namespace code differs between the primary ledger and the ledger that actually stores the subject. Reserved-namespace predicates (rdfs,rdf) survive because their codes are stable across ledgers; user-namespace predicates (e.g.abbrevName) are silently elided. This happens both for nested refs and for a top-level root whose subject lives in a non-primary ledger.Root cause
A dataset query is parsed/lowered once against the primary ledger's namespace dictionary, so every predicate
Sidin the projection spec carries the primary ledger's namespace code. When hydration renders a subject in another ledger, it re-encodes the subject IRI for the target view but passes the projectionlevelthrough unchanged. Those primary-dict predicateSids then miss the target ledger's index in the predicate filter and failselect_predicate'sSid-equality match — both silently.Fix
rebind_level_to_viewre-encodes the immediate projection level's predicateSids into the target ledger's namespace dictionary, so both the predicate filter andselect_predicatematch theSids as the target ledger actually stores them. It is applied at the two points a subject can be rendered against a non-primary view:expand_ref) — rebind at each cross-ledger crossing.format_hydration_column) — whenFormatterSet::pickroutes a root to its home (non-primary) ledger, rebind the level into that view's dict.Single-ledger and same-ledger (primary) subjects are untouched and byte-identical (no allocation). The re-encoding is memoized per
(view, level)for the response (HydrationCaches).Tests
cross_graph_nested_explicit_projection_divergent_predicate— the nested repro, now passing.cross_graph_root_projection_divergent_predicate— the root repro (red→green): a home-graph-bound root hydratesschema:name(shared code) but droppedp:fullNamepre-fix.cross_graph_wildcard_refinement_divergent_ns— covers the wildcard-refinement re-encode (previously uncovered).Out of scope (follow-up)
The non-primary hydration root routing case — a top-level result variable bound as the object of a primary-ledger triple, whose subject lives in another ledger — comes back
@id-only because routing itself fails to reach the home view (so even shared-code predicates are dropped). That's a distinct routing defect (FormatterSet::pickhas no home-ledger provenance for a bare object SID), left as an#[ignore]d repro (cross_graph_root_bound_as_object_hydrates_in_home_ledger) pending a separate fix. See the note on #1295.🤖 Generated with Claude Code