Skip to content

refactor: deterministic literal property targets + indexed WHERE filters#837

Draft
HexaField wants to merge 13 commits into
devfrom
refactor/literal-channel-v-separation
Draft

refactor: deterministic literal property targets + indexed WHERE filters#837
HexaField wants to merge 13 commits into
devfrom
refactor/literal-channel-v-separation

Conversation

@HexaField
Copy link
Copy Markdown
Contributor

@HexaField HexaField commented Jun 3, 2026

Summary

Two coupled changes to how resolveLanguage='literal' property values flow through the executor.

Property writes become deterministic. Today, setting a literal-language property routes through LanguageController.expression_create, which signs the value into a literal:json:<envelope> URL containing {author, timestamp, data, proof} — the same author + timestamp the link reifier already carries. The signature varies per write, so writing name = "general" produces a different IRI every time, and exact-match SPARQL filters never hit. This PR drops the wrap: resolve_property_value emits a plain literal:string:X (or :number: / :boolean: / :json: for primitives) URL via literal_encode directly. The reifier remains the single source of truth for per-link provenance.

WHERE filters use the index. With deterministic IRIs in storage, the WHERE builders for the model query layer can emit direct IRI matches (?source <pred> <literal:string:X>) instead of wrapping every target in a custom fn/parse_literal SPARQL function call. Native POS-index probe replaces a per-row function call.

cc @lucksus @data-bot-coasys — lands on top of #803's history. Commit range a9e98ccd..a00b595b iterated this same territory; the 353a7ad7 revert was driven by integration tests that codified the envelope shape, which this PR restructures.

What changed

Writes (executor)

  • perspective_instance.rs::resolve_property_value — when resolve_language == "literal", encodes the value directly via literal_encode and returns the plain URL. Other resolveLanguage values (real language controllers — note, image, etc.) still go through expression_create; those produce addressable signed expressions by design.
  • languages/literal.rs::literal_decode — no longer wraps primitive return values in a synthetic {author: "<unknown>", timestamp: "<unknown>", data: …, proof: {}} envelope. Returns the raw value. Genuine signed expressions (JSON objects) pass through unchanged.
  • core/src/perspectives/SparqlBindings.ts::parseLit — drops the .data extraction branch. With property values now stored as plain literals, the Literal.fromUrl(val).get() result is already the value, and the only objects it returns are real JSON payloads (which we JSON-stringify for display).

Migration

  • Restored migrate_signed_envelopes_to_plain_literals (originally landed in a9e98ccd, removed by 353a7ad7). On startup it walks every reifier, finds targets shaped as literal:json:<envelope> whose JSON has data+author+proof, extracts the inner value, and rewrites both the direct triple and the reifier (the reifier IRI hash includes the target, so it must be recomputed). Idempotent — short-circuits at migration_version >= 3. Wired into initialize_from_db after the named-graphs migration.
  • Envelope-unwrap branches retained in parse_literal_fn and parse_literal_value so SPARQL queries continue to handle pre-migration data + the small set of expressions that are themselves stored as signed envelopes (e.g. entanglement proofs in the dapp).

Indexed WHERE filters

  • model_query/sparql_builder.rsis_literal_prop equality WHERE filters emit ?source <pred> <literal:string:X_encoded> directly. String / number / boolean / StringArray / NumberArray covered. Ops (gt/lt/between/contains/not) keeps fn/parse_literal since it needs typed comparison.
  • Fallback for constructor-default raw IRIs (e.g. state = 'todo://ready' on a resolveLanguage='literal' property — the #803 d3da07d7 case): { ?source <pred> <literal:string:encoded> } UNION { ?source <pred> <raw-iri> }. Both branches are POS-index probes; the planner picks one.
  • model_query/projection.rs::build_projection_where_patterns — same transformation for the projection WHERE-clause builder. pred_lookup widened to carry is_literal_prop.

Tests

  • tests/js/tests/prolog-and-literals.test.ts — three round-trip assertions flipped from expect(literal.data).to.equal(X) to expect(literal).to.equal(X) to match the new (un-wrapped) Literal.fromUrl().get() return.
  • model_query/integration_tests.rs — the helper that built envelope-shaped targets and the tests that asserted query behaviour against them are renamed legacy_envelope_* and now call migrate_signed_envelopes_to_plain_literals before querying (genuine migration tests, not "queries work on envelope storage" tests). Parallel plain_literal_* tests cover the post-migration form.

What did NOT change

  • expression.create(value, "literal") still produces signed envelopes wrapped as literal:json:<envelope>. ExpressionClient.get(url)'s envelope fast-path is preserved.
  • Agent profile expression API (create_signed_expression for link metadata) is untouched.
  • SDNA payload literals (Prolog source strings, SHACL action JSON) — already plain literals before this PR, still plain.
  • Multi-user expression-authoring tests — unchanged contract.

Performance

Microbenchmark (bench_indexed_iri_vs_fn_parse_literal_filter in model_query/integration_tests.rs, gated to release builds, scale via WT_BENCH_LINKS) running the two equivalent SPARQL forms against the same literal:string: data on this branch:

n links indexed direct-IRI fn/parse_literal FILTER speedup
1,000 11.4 µs 290.8 µs 25.5×
5,000 12.5 µs 1,476.4 µs 118.4×
10,000 13.5 µs 3,046.1 µs 226.2×
20,000 15.8 µs 6,215.0 µs 394.2×
50,000 39.0 µs 19,436.0 µs 498.8×

Indexed stays flat (POS-index probe — sub-linear in the result count, ~unchanged by the dataset size). FILTER scales linearly because every row materialises through the custom function. Apple Silicon, 5 runs each, warm cache; reproduce with cargo test --release --lib bench_indexed_iri_vs_fn_parse_literal_filter -- --nocapture.

Wind tunnel S8 comparison vs origin/dev was attempted but the cached CUSTOM_DENO_SNAPSHOT.bin produces a V8 magic-number mismatch against fresh executor builds (Check failed: magic_number_ == SerializedData::kMagicNumber), causing the executor to crash on language-runtime init under the wind tunnel's cargo build --release path regardless of branch. That's a tunnel-infrastructure issue unrelated to this PR; the microbench above captures the relevant per-query delta directly.

Test plan

  • cargo check --tests clean
  • cargo test --lib languages::literal — 4 pass
  • cargo test --lib perspectives::model_query::utils — 15 pass
  • cargo test --lib perspectives::sparql_store — 78 pass
  • cargo test --lib perspectives::model_query — 146 pass (incl. the renamed legacy_envelope_migrated_then_* + new plain_literal_* tests)
  • pnpm exec tsc --noEmit (core) — clean
  • pnpm exec jest src/Literal.test.ts — 5 pass
  • Full CircleCI suite green on this PR
  • Wind tunnel s1/s2/s5/s8 + WHERE-heavy scenarios — dev vs this branch
  • Integration test runner (tests/js) — full suite
  • Manual smoke: spin executor against existing dataset with envelope-encoded literals, verify migration log fires + post-migration query returns expected results

Migration safety

  • Idempotent: migration_version >= 3 short-circuits.
  • Per-link transactional: each envelope-link replacement is one insert + one delete batch. A crash mid-migration leaves a mix of pre- and post-migration data — both readable thanks to the back-compat envelope-unwrap branches in parse_literal_fn and parse_literal_value.
  • Only touches reifier targets where the JSON parses, has .data+.author+.proof, and the new encoded form differs from the old. Won't accidentally rewrite valid literal:json: payloads that aren't envelopes.

Follow-ups

  • Typed RDF literals on the wire ("value"^^xsd:string instead of <literal:string:value>) and removing fn/parse_literal registration entirely — tackled in #842, stacked on this PR.
  • Pushing last-write-wins / hydration aggregation into SPARQL (currently in Rust) — explored in #846. The straightforward nested-aggregate plan hits an Oxigraph 0.5.8 planner cliff (the warning in ac57680b9 was confirmed empirically at ~23,000× regression). The most plausible unblock is storage-level partitioning from named graphs (#812); re-attempt once that lands and bench whether per-graph scoping flattens the planner working set. Note: SPARQL itself does not have window functions — neither 1.1 nor the W3C 1.2 draft introduces them — so the wait is on Oxigraph planner / dataset semantics, not on a spec change.
  • Flux read sites that assumed the envelope shape via .get().data and write sites that produced envelopes via client.expression.create(value, "literal") — handled in coasys/flux#604.

HexaField and others added 8 commits June 4, 2026 00:01
…lue (V1)

Part of the Channel V / Channel E separation. See
~/.sovereign/membranes/coasys/research/literal-encoding-and-sparql-pushdown-2026-06-03.md
§9.1 for the audit.

When resolve_language == "literal", encode the value directly via
literal_encode and return a deterministic plain URI
(literal:string:X / :number: / :boolean: / :json:) instead of going
through LanguageController.expression_create. Provenance for property
values lives on the link reifier — the signed-envelope shape is for
Channel E (expression.create) callers, not for property storage.

For all other resolve_language values (real language controllers —
note, image, etc.) the existing expression_create flow is unchanged.
literal_decode used to wrap non-object primitives in a fake
{author: "<unknown>", timestamp: "<unknown>", data: value, proof: {}}
envelope. That conflated Channel V (link-property values; provenance
lives on the link reifier) with Channel E (signed expressions).

Now primitives decode to their raw JSON value and objects pass through
unchanged. Channel-E callers that legitimately want envelope shape go
through create_signed_expression, not literal_decode.

Unit tests updated to assert raw round-trip; the JSON-object test is
unchanged.
After the Channel V refactor, Literal.fromUrl(val).get() for property
values returns the plain primitive directly. The only objects it can
return now are legitimate JSON objects from literal:json: payloads,
which we JSON-stringify for display.

Drops the .data extraction branch that used to unwrap signed-envelope
literals — those no longer exist for new property writes.
After the Channel V refactor, properties with resolveLanguage: "literal"
store their values as plain literal:string:X / :number: / :boolean: /
:json: URIs — no signed envelope. Tests now assert the value rather
than the envelope shape.

Renames the long-value test's local `expression` to `literal` for
consistency with the other two cases.
…ack-compat read path retained)

Restores migrate_signed_envelopes_to_plain_literals (originally added
in a9e98cc, removed by 353a7ad). Walks every reifier, finds targets
of the form literal:json:<envelope> whose JSON has data+author+proof,
extracts .data, re-encodes as the appropriate plain literal type, and
rebuilds both the direct triple and the reifier (the reifier IRI hash
includes the target, so it must be recomputed). Removes the old direct
triple only if no other reifiers still reference it. Migration is
idempotent — short-circuits when migration_version() >= 3 — and runs
during perspective initialization right after the named-graphs
migration (v2 -> v3).

Also annotates the back-compat envelope-unwrap branches in
parse_literal_fn (sparql_store.rs) and parse_literal_value
(model_query/utils.rs) with a note that they exist for pre-migration
data; new writes use plain literal: forms.

See §9.1 / §9.6 of literal-encoding-and-sparql-pushdown-2026-06-03.md.
Equality WHERE filters on `resolveLanguage: literal` properties now emit
direct IRI matches against the deterministic encoding produced by
`resolve_property_value` (Phase 1):

  ?source <pred> <literal:string:VAL_ENCODED> .
  ?source <pred> <literal:number:N> .
  ?source <pred> <literal:boolean:true|false> .

Previously each row was checked via `STR(fn/parse_literal(?v)) = "val"`
inside a FILTER, which forced Oxigraph to scan every triple bound to the
predicate. Switching to a direct IRI lets the POS index probe straight to
the matching row.

The `fn/parse_literal` BIND + FILTER path is preserved for:
- `WhereCondition::Ops` (gt/lt/gte/lte/between/contains/not) — typed
  comparison still needs the unwrapped value.
- Properties without `resolveLanguage: literal` — back-compat for raw
  string storage.
- Legacy envelope-form data that hasn't yet been migrated by
  `migrate_signed_envelopes_to_plain_literals` (Phase 2) — `parse_literal`
  also unwraps `.data` from signed envelopes.

Where a where-value also looks like an absolute IRI (scheme + colon),
a UNION fallback matches the raw-IRI form too — covers the case where a
constructor's initial value was stored as a raw URI on a property whose
shape declares `resolveLanguage: literal` (enum-like state defaults).

Values are validated before injection: strings are
`NON_ALPHANUMERIC`-percent-encoded (matching `literal_encode`), numbers
must be finite, booleans must be `true|false`. Non-finite numeric
filters short-circuit to `FILTER(false)`.

Refs: research/literal-encoding-and-sparql-pushdown-2026-06-03.md (V4)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirror the V4 transformation in the projection where-clause builder
(`build_projection_where_patterns`). Equality on a target-shape property
declared `resolveLanguage: literal` now emits a direct IRI match against
`<literal:string:X>` / `<literal:number:N>` / `<literal:boolean:b>`
instead of `STR(fn/parse_literal(?v)) = "X"`.

The pred_lookup now carries `(predicate, is_literal_prop)` so the
projection layer can distinguish properties whose targets are stored as
literal IRIs (use POS-index probe) from those backed by raw URIs or
unknown storage (keep the fn/parse_literal fallback). String/StringArray
inputs that themselves look like absolute IRIs also include the raw-IRI
form via VALUES, matching V4's UNION fallback semantics.

Refs: research/literal-encoding-and-sparql-pushdown-2026-06-03.md (V5)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…s (T4)

Restructure the WHERE-clause regression tests around the Phase 1/2/3
storage contract:

- Rename helper `signed_envelope_literal` -> `legacy_envelope_literal`.
  The envelope form models pre-migration data shape only; it is not the
  current write path.
- `test_signed_envelope_where_paginate_count` ->
  `test_legacy_envelope_migrated_then_paginate_count`. Now seeds
  envelope-form data, runs `migrate_signed_envelopes_to_plain_literals`,
  then asserts the V4 direct-IRI WHERE probe finds the migrated rows.
- `test_mixed_plain_and_signed_envelope_where` ->
  `test_legacy_mixed_migrated_then_contains`. Same migration-then-query
  pattern; mixes pre-existing plain literals with envelope rows to verify
  the migration is idempotent on plain data.
- Add `test_plain_literal_where_paginate_count` — parallel coverage
  using plain literals from the start (no migration call). Asserts the
  V4 POS-index path returns identical results to the legacy migrated
  test.
- Add `test_plain_literal_contains_works_on_fn_parse_literal_path` —
  exercises `WhereOps::contains`, which still routes through
  `fn/parse_literal`, against plain `literal:string:X` storage. Confirms
  the fallback path still produces correct substring semantics.

`test_resolve_projections_where_filter_via_target_shape_property` is
unchanged — it already stored plain `literal:string:like_type_id123`
and continues to pass because V5 emits a direct IRI probe matching the
stored form.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 3, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 819193f5-b23c-4d0c-a9a9-84b3786eb8be

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/literal-channel-v-separation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

HexaField added 3 commits June 4, 2026 00:24
The previous pass left a number of comments referencing internal phase
labels (V1/V4/V5), a 'Channel V/E' design vocabulary, and a 2026-06-03
research doc path. Replace those with comments that describe what the
code does and why a reader can't infer it from the code itself.
Compares two equivalent SPARQL queries against the same data — the
indexed direct-IRI probe that the WHERE builders now emit, and the
fn/parse_literal-wrapped FILTER they used to emit. Gated to release
builds via cfg!(debug_assertions); scale tunable via WT_BENCH_LINKS.

Indexed stays flat at ~12-40us across 1k-50k links (POS index probe).
Filter scales linearly because every row materialises through the
custom function.
@HexaField HexaField changed the title refactor: Channel V / Channel E literal-encoding separation refactor: deterministic literal property targets + indexed WHERE filters Jun 4, 2026
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.

1 participant