Writing a provider
A provider is a pure translator over the closed tree grammar. Third-party providers are the point of the project — SQL is the first, not the only one. Applications surveys the targets a provider can reach and the other things the tree is good for.
The interface
interface QueryProvider {
readonly name: string;
capabilities(): Capabilities;
execute<T>(plan: QueryPlan, signal?: AbortSignal): Promise<T>;
explain?(plan: QueryPlan): Promise<string>;
}capabilities()declares which plan ops (and, optionally, which WellKnown calls) you translate.Queryableruns a capability pre-check before any I/O — recursively throughjoin/leftJoininner plans — and fails fast with a located error, so an unsupported op never reaches yourexecute.execute(plan)receives an immutableQueryPlan— asourceand an ordered list of ops (filter,map,orderBy,take,join/leftJoinwith a nested inner plan, executors, …), each carrying anExpr.- An
includeop carries a self-containedIncludeSpec— navigation name, target source, key pair, cardinality, nested children. Providers never read relation metadata; everything needed to fetch and attach is in the spec. - The plan also carries the context's
relationsmap, so predicates over navigations (u.orders?.some(o => …)) resolve inside your translator — the SQL core turns them into correlatedEXISTS/NOT EXISTSsubqueries via theTranslateEnvon itsTranslateContext.
Implementing includes
Fetch related rows however your backend likes (the SQL core batches WHERE key = ANY(…) per navigation); the key collection, grouping, copy-on-attach, and the canonical child order all live in shared helpers so every provider agrees on the result shape:
import { collectIncludes, collectKeys, attachChildren } from "@greffon/query";
const specs = collectIncludes(plan.ops); // merged across repeated include()
const keys = collectKeys(parents, spec.from, spec.nav); // distinct, non-null
const stitched = attachChildren(parents, spec, children, spec.from, spec.to);Attach only for row-shaped executors (toArray, first, single); scalar executors ignore includes.
Partial-evaluate first, then translate
Always fold captures before translating. partialEval resolves the live closure and collapses every param-free subtree to a Constant, leaving a residual tree of param-rooted data access, constants, and operations over them.
import { partialEval } from "@greffon/core";
const residual = partialEval({ body: op.expr.body, scope: op.expr.scope });After folding, translation is a walk over a finite node set. Reject what you can't translate with a coded error that names your provider and the offending call — never a guess, never a silent client-side fallback.
Conformance: the memory provider is the reference
The in-memory provider defines correct behavior. Your provider must produce the same results. The provider-author kit ships a conformance harness:
import { defaultRelations, runConformance } from "@greffon/query/testing";
const results = await runConformance((fixtures) => makeMyProvider(fixtures), {
fixtures, // users / orders / items arrays
relations: defaultRelations(), // the corpus include cases resolve against these
});
const failures = results.filter((r) => !r.equal);The corpus queries are wrapped in expr(), so they reify into real trees whenever the module runs under the build plugin — trace @greffon/core in the plugin's packages option, or import "@greffon/fallback/register" when no build step runs the tests. Every divergence the reference finds — LIKE escaping, null ordering, collation, null join keys — becomes a permanent regression fixture.
Values are parameters, not strings
If your target is a query language with injection risk, bind constants as parameters; never interpolate them. The SQL provider turns every Constant into a $n placeholder and escapes %, _, and \ in LIKE patterns.