> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wazoo.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom SPARQL engines

> Worlds is engine-agnostic: implement the SparqlEngineInterface with any query engine. Here is a complete, copy-paste Comunica-backed engine so nothing locks you into the built-in Wazoo engine.

The `Sdk` facade does not care which engine answers SPARQL. The
`sparqlEngine` option accepts any implementation of `SparqlEngineInterface`, and
the SDK ships one opinionated default: the zero-dependency
[WazooSparqlEngine](https://github.com/wazootech/sparql-engine) (SPARQL 1.1 &
1.2 over any RDF/JS store, with built-in timeout and abort handling).

If you need a different engine — Comunica's federation, actors, or
query-planning features, for example — implement the interface yourself. It is a
single method. Nobody is locked in.

This guide builds a complete Comunica-backed engine, wire by wire, using
[`@comunica/query-sparql-rdfjs-lite`](https://github.com/comunica/comunica)
directly. It does not rely on any Worlds-provided Comunica adapter.

## The contract

`SparqlEngineInterface` has one method:

```typescript theme={null}
execute(request: SparqlRequest): Promise<SparqlResponse>;
```

* `SparqlRequest` carries `query`, `baseIri?`, `timeoutMs?`, and `signal?` (an
  `AbortSignal` the caller uses to cancel an in-flight request).
* `SparqlResponse` is one of four shapes: `{ kind: "select", data }`,
  `{ kind: "ask", data }`, `{ kind: "construct", data }`, or `{ kind: "void" }`.

## Install

Comunica and its RDF/JS types are npm packages:

```bash theme={null}
deno add npm:@comunica/query-sparql-rdfjs-lite @rdfjs/types
```

## Implement the interface

The engine runs Comunica over the same RDF/JS store the quad facade uses, so
imports and SPARQL queries see one consistent dataset.

```typescript comunica-sparql-engine.ts theme={null}
import { QueryEngine } from "@comunica/query-sparql-rdfjs-lite";
import type * as rdfjs from "@rdfjs/types";
import type {
  SparqlAskResults,
  SparqlBinding,
  SparqlConstructResults,
  SparqlEngineInterface,
  SparqlRequest,
  SparqlResponse,
  SparqlSelectResults,
  SparqlValue,
} from "@worlds/sdk/sparql-engine";

/** The minimal Comunica result shape this adapter consumes. */
interface ComunicaQueryResult {
  resultType: string;
  execute(): Promise<unknown>;
  metadata?(): Promise<{ variables: rdfjs.Variable[] }>;
}

/** A single row from a Comunica bindings stream. */
interface ComunicaBinding {
  get(variable: string): rdfjs.Term | undefined;
}

/** Default SPARQL query timeout in milliseconds (matches the Wazoo engine). */
const DEFAULT_TIMEOUT_MS = 30_000;

export class ComunicaSparqlEngine implements SparqlEngineInterface {
  readonly #engine = new QueryEngine();

  constructor(private readonly store: rdfjs.Store) {}

  async execute(request: SparqlRequest): Promise<SparqlResponse> {
    // One controller composes the caller's signal with the timeout, so
    // whichever fires first cancels the whole request.
    const controller = new AbortController();
    const timer = setTimeout(
      () => controller.abort(new Error("SPARQL query timed out")),
      request.timeoutMs ?? DEFAULT_TIMEOUT_MS,
    );
    const onCallerAbort = () =>
      controller.abort(
        request.signal?.reason instanceof Error
          ? request.signal?.reason
          : new Error("SPARQL query aborted"),
      );
    if (request.signal?.aborted) onCallerAbort();
    else
      request.signal?.addEventListener("abort", onCallerAbort, { once: true });

    try {
      return await Promise.race([
        this.#run(request, controller.signal),
        abortOn(controller.signal),
      ]);
    } finally {
      clearTimeout(timer);
      request.signal?.removeEventListener("abort", onCallerAbort);
    }
  }

  async #run(
    request: SparqlRequest,
    signal: AbortSignal,
  ): Promise<SparqlResponse> {
    const result = (await this.#engine.query(request.query, {
      sources: [this.store],
      baseIRI: request.baseIri,
    })) as ComunicaQueryResult;

    switch (result.resultType) {
      case "bindings":
        return { kind: "select", data: await this.#bindings(result) };
      case "boolean":
        return { kind: "ask", data: await this.#boolean(result) };
      case "quads":
        return { kind: "construct", data: await this.#quads(result) };
      case "void":
        await result.execute();
        return { kind: "void" };
      default:
        throw new Error(
          `Unsupported Comunica result type: ${result.resultType}`,
        );
    }
  }

  async #bindings(result: ComunicaQueryResult): Promise<SparqlSelectResults> {
    if (!result.metadata) {
      throw new Error("SPARQL bindings result is missing metadata.");
    }
    const metadata = await result.metadata();
    const vars = metadata.variables.map((variable) => variable.value);
    const stream = (await result.execute()) as AsyncIterable<ComunicaBinding>;

    const bindings: SparqlBinding[] = [];
    for await (const binding of stream) {
      const row: SparqlBinding = {};
      for (const variable of vars) {
        const term = binding.get(variable);
        if (term) row[variable] = termToSparqlValue(term);
      }
      bindings.push(row);
    }
    return { head: { vars }, results: { bindings } };
  }

  async #boolean(result: ComunicaQueryResult): Promise<SparqlAskResults> {
    const value = await result.execute();
    if (typeof value !== "boolean") {
      throw new Error("Comunica returned a non-boolean ASK result.");
    }
    return { head: {}, boolean: value };
  }

  async #quads(result: ComunicaQueryResult): Promise<SparqlConstructResults> {
    const quads: rdfjs.Quad[] = [];
    for await (const quad of (await result.execute()) as AsyncIterable<rdfjs.Quad>) {
      quads.push(quad);
    }
    return { quads };
  }
}

/** Maps an RDF/JS term to the SPARQL results JSON value shape. */
function termToSparqlValue(term: rdfjs.Term): SparqlValue {
  switch (term.termType) {
    case "NamedNode":
      return { type: "uri", value: term.value };
    case "BlankNode":
      return { type: "bnode", value: term.value };
    case "Literal": {
      const value: SparqlValue = { type: "literal", value: term.value };
      if (term.language) value["xml:lang"] = term.language;
      if (term.datatype.value !== "http://www.w3.org/2001/XMLSchema#string") {
        value.datatype = term.datatype.value;
      }
      return value;
    }
    case "Quad":
      return {
        type: "triple",
        value: {
          subject: termToSparqlValue(term.subject),
          predicate: termToSparqlValue(term.predicate),
          object: termToSparqlValue(term.object),
        },
      };
    default:
      throw new Error(`Unsupported RDF term type: ${term.termType}`);
  }
}

/** Rejects with the controller's reason when the signal fires. */
function abortOn(signal: AbortSignal): Promise<never> {
  return new Promise((_, reject) => {
    if (signal.aborted) {
      reject(
        signal.reason instanceof Error
          ? signal.reason
          : new Error("SPARQL query aborted"),
      );
      return;
    }
    signal.addEventListener(
      "abort",
      () =>
        reject(
          signal.reason instanceof Error
            ? signal.reason
            : new Error("SPARQL query aborted"),
        ),
      { once: true },
    );
  });
}
```

## Wire it into a client

Swap the engine in the same way you would configure any other
`SparqlEngineInterface`:

```typescript index.ts theme={null}
import { Sdk } from "@worlds/sdk";
import { RdfjsQuadStore, RdfjsSearchIndex } from "@worlds/sdk/rdfjs";
import { Store } from "n3";
import { ComunicaSparqlEngine } from "./comunica-sparql-engine.ts";

const store = new Store();
const client = new Sdk({
  quadStore: new RdfjsQuadStore({ store }),
  searchIndex: new RdfjsSearchIndex(store),
  sparqlEngine: new ComunicaSparqlEngine(store),
});
```

The engine handles SELECT, ASK, CONSTRUCT/DESCRIBE, and SPARQL UPDATE, maps
binding terms into the standard SPARQL results JSON shape, and honors
`timeoutMs` and `signal` exactly as the interface documents.

## Notes

* **Transactional updates.** Comunica writes directly to the store. If your
  backend needs atomic writes through its own transaction (like the built-in
  engine's `createTransaction`), wrap the read store in a
  `TransactionalRdfjsStore` from `@worlds/sdk/quad-store`, hand that to the
  engine, and commit on `void` responses.
* **When to stay on Wazoo.** The built-in `WazooSparqlEngine` is zero-dependency
  and covers SPARQL 1.1 & 1.2 for in-process RDF/JS stores. Reach for a custom
  engine when you need capabilities it does not provide — for example Comunica's
  federation or actor customization.
* **No SDK changes needed.** The adapter lives entirely in your project; the
  Worlds SDK only defines the interface.
