Skip to content

Federated Queries

IndentiaDB can act as a federated query hub, transparently querying remote SPARQL endpoints, virtual SQL-to-RDF graphs, and its own local triple store in a single SPARQL query. Results are merged and returned to the client as if all data lived in one place.

SERVICE is disabled by default

Federated SERVICE queries on the /sparql endpoint ship dark. Until the server runs with IG_SPARQL_FEDERATION=true, any query containing SERVICE is answered with 501 Not Implemented and no remote endpoint is contacted. See Configuration & Security before enabling it.


SPARQL SERVICE Clause

The SPARQL SERVICE clause delegates a sub-query to a named remote endpoint. IndentiaDB implements SPARQL 1.1 and 1.2 Federated Query: it sends the sub-query to the remote endpoint over HTTP, collects the results, and joins them with the local results. RDF 1.2 values — triple terms and directional language-tagged literals — survive the round trip in both directions.

Basic Federated Query

PREFIX foaf:   <http://xmlns.com/foaf/0.1/>
PREFIX schema: <http://schema.org/>

SELECT ?person ?name ?description WHERE {
    # Local data: people in IndentiaDB
    ?person a foaf:Person ;
            foaf:name ?name .

    # Remote data: description from DBpedia
    SERVICE <https://dbpedia.org/sparql> {
        ?person schema:description ?description .
        FILTER(lang(?description) = "en")
    }
}
LIMIT 20

SERVICE Endpoint Must Be Reachable and Allowed

The endpoint URI in SERVICE must be accessible from the machine running IndentiaDB and permitted by the federation endpoint policy (see Configuration & Security). In Kubernetes deployments, ensure egress network policies allow outbound HTTP/HTTPS to the remote endpoint.

SERVICE SILENT (Fault-Tolerant Federation)

Add SILENT to continue with local results even if the remote endpoint is unavailable or returns an error:

PREFIX foaf:   <http://xmlns.com/foaf/0.1/>
PREFIX owl:    <http://www.w3.org/2002/07/owl#>

SELECT ?person ?name ?sameAs WHERE {
    ?person a foaf:Person ;
            foaf:name ?name .

    # Try to fetch sameAs links from Wikidata; silently skip if unavailable
    OPTIONAL {
        SERVICE SILENT <https://query.wikidata.org/sparql> {
            ?wikidataItem owl:sameAs ?person .
            BIND(?wikidataItem AS ?sameAs)
        }
    }
}

If the remote endpoint times out or returns an error, ?sameAs is simply unbound — the query succeeds with local data only. Without SILENT, the same failure fails the whole query with an HTTP error status (see HTTP Status Codes).

Federated Query with DBpedia

PREFIX dbo:    <http://dbpedia.org/ontology/>
PREFIX dbr:    <http://dbpedia.org/resource/>
PREFIX foaf:   <http://xmlns.com/foaf/0.1/>
PREFIX ex:     <http://example.org/>

SELECT ?researcher ?name ?birthDate ?institution ?hIndex WHERE {
    # Local: researchers and their h-index (stored locally)
    ?researcher a ex:Researcher ;
                foaf:name ?name ;
                ex:hIndex ?hIndex .
    FILTER(?hIndex > 20)

    # Remote: biographical data from DBpedia
    SERVICE <https://dbpedia.org/sparql> {
        ?researcher dbo:birthDate  ?birthDate .
        OPTIONAL { ?researcher dbo:institution ?institution . }
    }
}
ORDER BY DESC(?hIndex)
LIMIT 10

Multiple SERVICE Clauses in One Query

PREFIX foaf:   <http://xmlns.com/foaf/0.1/>
PREFIX dbo:    <http://dbpedia.org/ontology/>
PREFIX wd:     <http://www.wikidata.org/entity/>
PREFIX wdt:    <http://www.wikidata.org/prop/direct/>
PREFIX ex:     <http://example.org/>

SELECT ?person ?name ?birthDate ?occupation ?awards WHERE {
    # Local store
    ?person a foaf:Person ;
            foaf:name ?name .

    # DBpedia for biographical facts
    OPTIONAL {
        SERVICE <https://dbpedia.org/sparql> {
            ?person dbo:birthDate ?birthDate .
            OPTIONAL { ?person dbo:occupation ?occupation . }
        }
    }

    # Wikidata for awards
    OPTIONAL {
        SERVICE SILENT <https://query.wikidata.org/sparql> {
            ?wikidataItem wdt:P31 wd:Q5 ;
                          wdt:P166 ?awards .
            FILTER(STR(?wikidataItem) = STR(?person))
        }
    }
}
LIMIT 25

Each SERVICE clause is evaluated as described in How SERVICE Is Evaluated, and its results are joined with the local solutions in-process.


Configuration & Security

Federation is configured with environment variables at server start. All defaults are behavior-neutral: without IG_SPARQL_FEDERATION, nothing changes.

Flag Default Effect
IG_SPARQL_FEDERATION off true or 1 enables SERVICE execution on /sparql — on the buffered and the streaming result paths.
IG_SPARQL_FEDERATION_ALLOWLIST empty Comma-separated endpoint patterns. When set, only matching endpoints may be called. When empty, the secure default policy applies: public endpoints are allowed, while private, loopback and cloud-metadata addresses are blocked.
IG_SPARQL_FEDERATION_AUTH unset JSON array of per-endpoint credentials for outbound calls (see below).
IG_SPARQL_FEDERATION_ALLOW_PRIVATE off Explicit opt-in for federation between IndentiaDB instances inside one network-fenced cluster or VPC: lifts the block on private and loopback addresses. Cloud-metadata endpoints stay blocked.
IG_SPARQL_FEDERATION=true
IG_SPARQL_FEDERATION_ALLOWLIST=https://query.wikidata.org/,https://dbpedia.org/sparql,https://*.partner.example/sparql
IG_SPARQL_FEDERATION_AUTH='[{"endpoint":"https://sparql.partner.example/","type":"bearer","token":"<from-secret>"}]'

Endpoint Policy

Every SERVICE call is checked before a connection is made:

  • Only http and https URLs are accepted; URLs with embedded user info (https://user@host/…) are refused.
  • Private, loopback and non-global addresses are blocked (unless IG_SPARQL_FEDERATION_ALLOW_PRIVATE is set), and cloud-metadata addresses are always blocked. Host names are resolved and every returned address is validated; the validated addresses are the ones used for the connection, which defeats DNS-rebinding tricks.
  • Redirects of uncredentialed calls are re-validated against the same policy.

Allowlist pattern semantics:

Pattern Matches
No * — prefix URLs with the same scheme, host and port whose full URL starts with the pattern. https://allowed.example does not match https://allowed.example.attacker.test.
With * — glob * matches any run of characters, but inside the scheme/host/port part it never crosses / or @: https://*.wikidata.org/* matches https://query.wikidata.org/sparql but not https://evil.example/x.wikidata.org/. A glob without an explicit scheme never matches.
* Every endpoint (still subject to the address blocks above).

Changed behavior

Earlier releases let a * glob span the URL authority boundary. Review existing IG_SPARQL_FEDERATION_ALLOWLIST and IG_SPARQL_FEDERATION_AUTH patterns when upgrading.

With IG_PRODUCTION=true, enabling federation requires a non-empty allowlist whose entries are explicit https:// URLs with a host, without wildcards and without user info; the server refuses to boot otherwise.

Per-Endpoint Credentials

IG_SPARQL_FEDERATION_AUTH holds a JSON array. Each entry pairs an endpoint pattern (same prefix/glob semantics as the allowlist) with one credential; the first matching entry wins.

[
  { "endpoint": "https://sparql.partner.example/", "type": "bearer", "token": "…" },
  { "endpoint": "https://kb.example.org/*", "type": "basic", "username": "fed", "password": "…" },
  { "endpoint": "https://api.example.net/sparql", "type": "header", "name": "X-API-Key", "value": "…" },
  { "endpoint": "http://ig-peer.internal:7001/", "allow_http": true, "type": "bearer", "token": "…" }
]
type Fields Sent as
bearer token Authorization: Bearer <token>
basic username (no :), password HTTP Basic authentication
header name, value An arbitrary header, e.g. an API key

Credentials are fail-closed:

  • A credential is only attached over HTTPS. A matching http:// endpoint is refused (403) unless that entry sets "allow_http": true — intended for network-fenced in-cluster peers only.
  • Credentialed requests never follow redirects, so a secret cannot be replayed to a redirect target.
  • A malformed value — invalid JSON, an unknown type, a value that is not a valid header, or a glob pattern without an explicit scheme before the first * — refuses server boot instead of running unauthenticated.
  • Secrets are redacted from logs and debug output. Store the JSON in a secret, not in plain configuration.

HTTP Status Codes

Status Cause
501 Not Implemented The query uses SERVICE, but federation is disabled on this server
400 Bad Request The SERVICE IRI is unbound or invalid
403 Forbidden The endpoint is refused by the endpoint policy, or a credential would be sent over plain HTTP
502 Bad Gateway The remote endpoint failed: connection error, HTTP error status, unsupported content type, unparseable results, or a response above the size limit
504 Gateway Timeout The remote endpoint did not answer in time (see limits)

The remote response body is never echoed back to the client. With SERVICE SILENT, a failing remote call contributes no bindings instead of failing the query.


How SERVICE Is Evaluated

  1. Bind join (VALUES pushdown) — for Join(local, SERVICE { P }) and its OPTIONAL form, when the local side shares variables with P, IndentiaDB evaluates the local side first, collects the distinct shared bindings and sends a single request of the form SELECT * WHERE { VALUES (…) { … } P }. The remote endpoint returns only rows that can join. This applies to buffered and streaming responses alike.
  2. Materialize once — without shared variables, or with more than 10,000 distinct bindings to push, the remote pattern is fetched exactly once and joined locally instead of being requested once per local solution.
  3. Per-pattern fallback — any shape the rewrite cannot handle safely (for example results with blank nodes, unusual algebra, or a failed rewrite) is evaluated by the standard SERVICE handler with full SPARQL semantics, including SILENT.

All strategies preserve query semantics:

  • Join multiplicity is preserved — duplicate remote rows produce duplicate joined rows, exactly as the SPARQL algebra requires.
  • Blank nodes from a remote result are scoped to that response: equal labels in two different responses, or a remote label equal to a local one, never join.
  • The request sent to the remote endpoint carries the query's BASE, and a SERVICE inside GRAPH, FROM or FROM NAMED evaluates its local side against that same dataset scope.

Limits:

Limit Value
Request timeout 30 seconds per remote request. Requests issued by the bind-join and materialize-once strategies are additionally bounded by the remaining time of the query's own timeout; such a fetch is not sent at all once that deadline has passed.
Connect timeout 10 seconds
Maximum remote response size 100 MB (larger responses fail with 502)

Remote SERVICE results are not cached between queries.

When federation is enabled, GET /sparql without a query parameter returns a SPARQL Service Description that advertises sd:BasicFederatedQuery.


Source Types

Embedded federation engine

The source types, Rust API and R2RML virtual graphs below belong to the embedded Rust federation engine (indentiagraph_surreal::federation) for applications that embed IndentiaDB. They are configured in code and are independent of the SERVICE support on the /sparql endpoint and its IG_SPARQL_FEDERATION* settings described above.

Source Implementation Protocol Description
LocalTripleSource Native Direct The local IndentiaDB triple store. Always available.
SparqlTripleSource HTTP/HTTPS SPARQL 1.1 Any remote SPARQL endpoint (DBpedia, Wikidata, Stardog, Virtuoso, Qlever, etc.)
SQL Database R2RML JDBC-style Relational DB exposed as virtual RDF triples via R2RML mapping
REST API Planned HTTP+JSON REST-to-RDF bridge for REST APIs without a SPARQL interface

Rust API for Federation

Configure federation sources programmatically when embedding IndentiaDB in a Rust application:

use indentiagraph_surreal::federation::{
    EndpointAuth, FederationEngine, FederationSource, SourceCapabilities, SourceType,
    SurrealFederationEngine,
};

// `store` is the embedded Arc<SurrealTripleStore>; it becomes the local source.
let engine = SurrealFederationEngine::new(store.clone());

// Remote SPARQL endpoint (DBpedia)
engine
    .register_source(FederationSource {
        id: "dbpedia".into(),
        name: "DBpedia".into(),
        source_type: SourceType::SparqlEndpoint {
            url: "https://dbpedia.org/sparql".into(),
            auth: None,
        },
        capabilities: SourceCapabilities::sparql_endpoint(),
        available: true,
        priority: 10, // lower = preferred
    })
    .await?;

// Remote SPARQL endpoint requiring authentication
engine
    .register_source(FederationSource {
        id: "partner".into(),
        name: "Partner knowledge graph".into(),
        source_type: SourceType::SparqlEndpoint {
            url: "https://sparql.partner.example/query".into(),
            auth: Some(EndpointAuth::Bearer {
                token: std::env::var("SPARQL_TOKEN")?,
            }),
        },
        capabilities: SourceCapabilities::sparql_endpoint(),
        available: true,
        priority: 20,
    })
    .await?;

// Execute a federated query across the registered sources
let results = engine
    .query("SELECT * WHERE { ?s ?p ?o } LIMIT 10")
    .await?;

EndpointAuth also supports Basic { username, password } and ApiKey { header, key }.


R2RML Virtual Graphs (SQL → RDF)

R2RML (RDB to RDF Mapping Language) is a W3C standard for exposing relational database tables as virtual RDF graphs, queryable via SPARQL without physically materializing the triples.

IndentiaDB evaluates R2RML-mapped triples at query time: when a SPARQL query touches a predicate or class that is defined in an R2RML mapping, IndentiaDB rewrites the triple pattern into an SQL query and executes it against the configured relational database.

Full R2RML Mapping Example

This maps an employees table to foaf:Person and ex:Employee triples:

@prefix rr:   <http://www.w3.org/ns/r2rml#> .
@prefix ex:   <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix org:  <http://www.w3.org/ns/org#> .

# Map the employees table
ex:EmployeeMapping a rr:TriplesMap ;

    rr:logicalTable [
        rr:tableName "employees"
    ] ;

    rr:subjectMap [
        rr:template  "http://company.com/employee/{id}" ;
        rr:class     ex:Employee ;
        rr:class     foaf:Person ;
    ] ;

    # Full name
    rr:predicateObjectMap [
        rr:predicate foaf:name ;
        rr:objectMap [ rr:column "full_name" ; rr:language "en" ] ;
    ] ;

    # Email
    rr:predicateObjectMap [
        rr:predicate foaf:mbox ;
        rr:objectMap [
            rr:template  "mailto:{email}" ;
            rr:termType  rr:IRI ;
        ] ;
    ] ;

    # Department (foreign key → IRI)
    rr:predicateObjectMap [
        rr:predicate org:memberOf ;
        rr:objectMap [
            rr:template  "http://company.com/department/{department_id}" ;
            rr:termType  rr:IRI ;
        ] ;
    ] ;

    # Salary (typed literal)
    rr:predicateObjectMap [
        rr:predicate ex:salary ;
        rr:objectMap [
            rr:column   "salary" ;
            rr:datatype xsd:decimal ;
        ] ;
    ] ;

    # Hire date
    rr:predicateObjectMap [
        rr:predicate ex:hireDate ;
        rr:objectMap [
            rr:column   "hire_date" ;
            rr:datatype xsd:date ;
        ] ;
    ] ;

    # Active flag
    rr:predicateObjectMap [
        rr:predicate ex:isActive ;
        rr:objectMap [
            rr:column   "is_active" ;
            rr:datatype xsd:boolean ;
        ] ;
    ] .

# Map the departments table
ex:DepartmentMapping a rr:TriplesMap ;

    rr:logicalTable [
        rr:tableName "departments"
    ] ;

    rr:subjectMap [
        rr:template "http://company.com/department/{id}" ;
        rr:class    org:OrganizationalUnit ;
    ] ;

    rr:predicateObjectMap [
        rr:predicate foaf:name ;
        rr:objectMap [ rr:column "name" ] ;
    ] ;

    rr:predicateObjectMap [
        rr:predicate ex:budget ;
        rr:objectMap [ rr:column "budget" ; rr:datatype xsd:decimal ] ;
    ] .

Registering an R2RML Source

An R2RML source is registered on the embedded federation engine like any other source, with SourceType::SqlDatabase { connection_string, mapping } (see Rust API for Federation). SQL sources require building indentiagraph-surreal with the sql-federation Cargo feature. There is no server TOML section for registering R2RML sources.

Querying the Virtual Graph

Once registered, the R2RML source is queryable through the federation engine exactly like any native graph:

PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX ex:   <http://example.org/>
PREFIX org:  <http://www.w3.org/ns/org#>

SELECT ?name ?department ?salary WHERE {
    ?employee a ex:Employee ;
              foaf:name   ?name ;
              org:memberOf ?dept ;
              ex:salary    ?salary .
    ?dept foaf:name ?department .
    FILTER(?salary > 80000)
}
ORDER BY DESC(?salary)
LIMIT 50

IndentiaDB translates this SPARQL query into:

SELECT e.full_name AS name, d.name AS department, e.salary
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.salary > 80000
ORDER BY e.salary DESC
LIMIT 50;

This translation happens transparently — the SPARQL client never sees the SQL.

Supported Databases for R2RML

Database Driver Connection String Format
PostgreSQL sqlx postgresql://user:pass@host:5432/db
MySQL / MariaDB sqlx mysql://user:pass@host:3306/db
SQLite sqlx sqlite:///path/to/file.db

Supported R2RML Features

Feature Status Notes
rr:tableName Supported Direct table reference
rr:sqlQuery Supported Custom SQL as logical table
rr:template Supported IRI and literal templates with {column} placeholders
rr:column Supported Direct column reference
rr:constant Supported Constant IRI or literal value
rr:class Supported rdf:type triple shorthand
rr:datatype Supported Typed literals (xsd:integer, xsd:date, etc.)
rr:language Supported Language-tagged strings
rr:joinCondition Supported Foreign-key joins via rr:parentTriplesMap
R2RML Views Planned Named views as logical tables

R2RML is Read-Only

The R2RML virtual graph supports SPARQL SELECT and CONSTRUCT queries only. SPARQL UPDATE (INSERT DATA, DELETE DATA) against an R2RML-mapped source is not supported — write to the relational database directly via its native driver.

For how SERVICE clauses on the /sparql endpoint are optimized (bind join, materialize once, time and size limits), see How SERVICE Is Evaluated.