Skip to content

fix(query): cross-ledger explicit projection drops foreign-namespace predicates - #1417

Merged
Jackamus29 merged 9 commits into
mainfrom
fix/1295-cross-ledger-projection-rebind
Jul 2, 2026
Merged

fix(query): cross-ledger explicit projection drops foreign-namespace predicates#1417
Jackamus29 merged 9 commits into
mainfrom
fix/1295-cross-ledger-projection-rebind

Conversation

@Jackamus29

@Jackamus29 Jackamus29 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Closes #1295.

Problem

A cross-ledger JSON-LD explicit projection over a multi-ledger from silently 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 Sid in 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 projection level through unchanged. Those primary-dict predicate Sids then miss the target ledger's index in the predicate filter and fail select_predicate's Sid-equality match — both silently.

Fix

rebind_level_to_view re-encodes the immediate projection level's predicate Sids into the target ledger's namespace dictionary, so both the predicate filter and select_predicate match the Sids as the target ledger actually stores them. It is applied at the two points a subject can be rendered against a non-primary view:

  • Nested refs (expand_ref) — rebind at each cross-ledger crossing.
  • Root subjects (format_hydration_column) — when FormatterSet::pick routes 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 hydrates schema:name (shared code) but dropped p:fullName pre-fix.
  • cross_graph_wildcard_refinement_divergent_ns — covers the wildcard-refinement re-encode (previously uncovered).
  • Existing cross-ledger guards stay green: cross-graph IRI decode, depth-3 wildcard expansion, home-ledger property decode, and named-graph scoped projection.

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::pick has 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

Jackamus29 and others added 9 commits July 1, 2026 22:55
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
Jackamus29 force-pushed the fix/1295-cross-ledger-projection-rebind branch from d64894e to e2196e0 Compare July 2, 2026 02:59

@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.

👍

@Jackamus29
Jackamus29 merged commit a8d7ee8 into main Jul 2, 2026
13 checks passed
@Jackamus29
Jackamus29 deleted the fix/1295-cross-ledger-projection-rebind branch July 2, 2026 12:09
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.

Cross-ledger explicit projection silently drops foreign-namespace predicates

2 participants