Skip to content

SERVICE: a multi-pattern body returns zero rows while each pattern alone matches #1665

Description

@aaj3f

Found while building the provenance tests for #1641 and deliberately kept out of that PR's scope — filing separately as promised there.

A SERVICE body carrying two triple patterns on the same subject returns no rows at all, while either pattern alone matches, and the identical two-pattern body evaluated via GRAPH returns the expected rows. Since the GRAPH control passes over the same dataset wiring, it seems the defect lives in the SERVICE evaluation lane's handling of the intra-body BGP (the join/merge across the body's patterns) rather than in the data or dataset plumbing — though I haven't traced the exact locus, so I may be wrong about where it goes wrong, just not about the behavior.

Consequence: any cross-ledger query whose SERVICE body has more than one triple pattern silently returns empty — no error, just missing rows — which is exactly the kind of failure a user will attribute to their own data. It fails soft.

Repro — validated on main @ c8c1088 (clean detached worktree)

Setup: two in-memory ledgers, mp-beta:main queried via SERVICE.

  • mp-alpha:main: {"@id":"http://alpha.example/a1","http://alpha.example/tag":"shared"}
  • mp-beta:main: {"@id":"http://beta.example/b1","http://beta.example/tag":"shared","http://beta.example/rank":2}, {"@id":"http://beta.example/b2","http://beta.example/tag":"shared","http://beta.example/rank":1}
  • Dataset: mp-alpha:main default graph, mp-beta:main named graph (DatasetSpec::new().with_default(...).with_named(...), fluree.build_dataset_view).
-- 2 rows:
SELECT ?s WHERE { SERVICE <fluree:ledger:mp-beta:main> { ?s ?p "shared" } }

-- 2 rows:
SELECT ?s ?r WHERE { SERVICE <fluree:ledger:mp-beta:main> { ?s <http://beta.example/rank> ?r } }

-- 2 rows (control — same two-pattern body via GRAPH):
SELECT ?s ?r WHERE { GRAPH <mp-beta:main> { ?s ?p "shared" . ?s <http://beta.example/rank> ?r } }

-- 0 rows (the bug):
SELECT ?s ?r WHERE { SERVICE <fluree:ledger:mp-beta:main> { ?s ?p "shared" . ?s <http://beta.example/rank> ?r } }

Observed on c8c108838023c335398924469b1b240f87afcb90: the two-pattern SERVICE query returns [] while the GRAPH control returns [["http://beta.example/b1",2],["http://beta.example/b2",1]].

Integration test

The duplicate-issue sweep and the worktree validation at c8c1088 were Claude's work; pasting the test from that run as-is (drop into fluree-db-api/tests/ plus a [[test]] entry since autotests=false; the harness mirrors #1641's it_service_cross_ledger_iri.rs):

#[path = "support/mod.rs"]
mod support;

use fluree_db_api::{DataSetDb, DatasetSpec, FlureeBuilder, GraphSource};
use serde_json::{json, Value as JsonValue};
use support::{genesis_ledger, MemoryFluree};

const BETA: &str = "http://beta.example/";

async fn seed(fluree: &MemoryFluree, ledger_id: &str, graph: JsonValue) {
    let ledger0 = genesis_ledger(fluree, ledger_id);
    fluree.insert(ledger0, &json!({ "@graph": graph })).await.expect("seed");
}

async fn rows(fluree: &MemoryFluree, dataset: &DataSetDb, q: &str) -> Vec<JsonValue> {
    let result = fluree.query_dataset(dataset, q).await.unwrap_or_else(|e| panic!("query failed: {e}\n{q}"));
    let jsonld = result.to_jsonld(dataset.primary().unwrap().snapshot.as_ref()).expect("to_jsonld");
    let mut v = jsonld.as_array().cloned().unwrap_or_default();
    v.sort_by_key(|r| serde_json::to_string(r).unwrap_or_default());
    v
}

#[tokio::test]
async fn service_multi_pattern_body_returns_rows() {
    let fluree = FlureeBuilder::memory().build_memory();
    seed(&fluree, "mp-alpha:main",
        json!([{"@id": "http://alpha.example/a1", "http://alpha.example/tag": "shared"}])).await;
    seed(&fluree, "mp-beta:main",
        json!([
            {"@id": format!("{BETA}b1"), format!("{BETA}tag"): "shared", format!("{BETA}rank"): 2},
            {"@id": format!("{BETA}b2"), format!("{BETA}tag"): "shared", format!("{BETA}rank"): 1}
        ])).await;
    let spec = DatasetSpec::new()
        .with_default(GraphSource::new("mp-alpha:main"))
        .with_named(GraphSource::new("mp-beta:main"));
    let dataset = fluree.build_dataset_view(&spec).await.expect("build_dataset_view");

    let single_tag = rows(&fluree, &dataset,
        r#"SELECT ?s WHERE { SERVICE <fluree:ledger:mp-beta:main> { ?s ?p "shared" } }"#).await;
    let single_rank = rows(&fluree, &dataset,
        &format!(r#"SELECT ?s ?r WHERE {{ SERVICE <fluree:ledger:mp-beta:main> {{ ?s <{BETA}rank> ?r }} }}"#)).await;
    let graph_two = rows(&fluree, &dataset,
        &format!(r#"SELECT ?s ?r WHERE {{ GRAPH <mp-beta:main> {{ ?s ?p "shared" . ?s <{BETA}rank> ?r }} }}"#)).await;
    let service_two = rows(&fluree, &dataset,
        &format!(r#"SELECT ?s ?r WHERE {{ SERVICE <fluree:ledger:mp-beta:main> {{ ?s ?p "shared" . ?s <{BETA}rank> ?r }} }}"#)).await;

    assert_eq!(single_tag.len(), 2);
    assert_eq!(single_rank.len(), 2);
    assert_eq!(graph_two.len(), 2);
    assert_eq!(service_two, graph_two, "two-pattern SERVICE body must match its GRAPH control");
    // On origin/main c8c108838: service_two == [] while graph_two has 2 rows.
}

Two notes for whoever picks this up

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Next up: wrong results, live user pain, unblocksarea:queryQuery execution, planning, fast paths, overlay, result formatting

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions