README
¶
Query Gateway
A standalone Go library that resolves GLQL
— GitLab Query Language, the language behind glql Markdown blocks — over HTTP.
Owned by group::global search. GitLab Workhorse is a consumer: it mounts
this library's handler on the gateway route. The library was extracted out of
Workhorse so that group::source code does not own or maintain search-team
logic (discussion).
go get gitlab.com/gitlab-org/search-team/query-gateway
What it does
One HTTP request in, one GLQL result out. For each request the gateway:
- Compiles the GLQL query by running the GLQL transpiler WebAssembly module in-process (wazero, no cgo, no JavaScript engine).
- Routes the compiled query to a backend chosen by the compile mode the
transpiler reports —
standardcompiles to a GraphQL document,orbitcompiles to a graph-query traversal DSL body. - Resolves it by forwarding to that backend with the caller's original credentials untouched.
- Transforms the backend result back into the GLQL result shape.
Two design properties are load-bearing and covered by tests:
- No authorization here. The gateway holds no identity and caches nothing per user; every query is authorized per request by the backend that owns the data. That is what stops a cached result outliving a revoked permission.
- Backend identity never leaks. Which backend answered is an implementation
detail of a
Resolver. Provider names are screened out of responses and out of log lines (seequeryapi/backend_identity.go).
Public API
queryapi
| Symbol | Purpose |
|---|---|
NewHandler(transpiler Transpiler, timeout time.Duration, resolvers ...Resolver) *Handler |
Builds the gateway handler. A query whose compile mode has no registered resolver is refused with a contract-defined error rather than routed elsewhere. |
Handler.ServeHTTP(w, r) |
Standard http.Handler. |
Transpiler interface |
Compile(ctx, query, compileContext) and Transform(ctx, data, transformContext). Must be safe for concurrent use. Satisfied by glqlwasm.Transpiler. |
Resolver interface |
Mode() string plus Resolve(...) (Resolution, error). Adding a backend is a resolver registration, not a handler change. |
NewGraphQLResolver(backend *url.URL, rt http.RoundTripper) Resolver |
The standard-mode resolver: posts the compiled GraphQL document upstream. |
Resolution, UpstreamResponse, QueryErrors |
Result and error carriers. An *UpstreamResponse error carries a verbatim upstream reply that must be relayed unaltered. |
ModeStandard |
Compile-mode constant ("standard"). orbit mode is recognised by the router but has no exported constant or bundled resolver yet; register your own Resolver for it. |
MaxRequestBytes |
Inbound request-envelope cap, aligned with Analytics::Glql::ParserService::MAX_INPUT_SIZE. |
queryapi/glqlwasm
| Symbol | Purpose |
|---|---|
New(ctx, Options) (*Transpiler, error) |
Loads the .wasm module and pre-instantiates the instance pool. The AOT compile costs ~1.5s, so call this at process start-up, never in a request path. |
Options{WasmPath string, PoolSize int} |
PoolSize bounds concurrent transpilations; defaults to GOMAXPROCS. |
Transpiler.Compile / .Transform / .Close |
Implements queryapi.Transpiler. |
ErrDisabled, ErrPoolExhausted, ErrQueryTooLarge, ErrResultTooLarge |
Sentinel errors callers can branch on. |
MaxLegalTransformBytes |
Input ceiling for the transform stage. |
Each instance's wasm linear memory is capped at 256 MiB. Exceeding it produces a size error and the instance is retired and replaced, leaving the pool intact — memory exhaustion fails well rather than poisoning the process.
Wiring example
tr, err := glqlwasm.New(ctx, glqlwasm.Options{WasmPath: "/opt/glql/glql.wasm"})
if err != nil {
return err
}
defer tr.Close(ctx)
h := queryapi.NewHandler(tr, 30*time.Second,
queryapi.NewGraphQLResolver(railsGraphQLURL, http.DefaultTransport))
mux.Handle("/api/glql", h)
Obtaining the wasm module
The module is not committed to this repository. It is the same artifact the
GitLab frontend uses, and it ships as a base64 blob inlined into a rollup
bundle — npm/dist/main.js inside the
@gitlab/query-language-rust
npm package — rather than as a .wasm file. That is why obtaining it is an
extraction rather than a copy.
_support/extract_glql_wasm.rb performs the extraction: it locates the npm
package, pulls the base64 argument out of the bundle's _loadWasmModule(...)
call, decodes it, verifies the \x00asm magic number, and writes the module out.
# In a GitLab checkout that has had `yarn install` run:
GLQL_NODE_MODULES=/path/to/gitlab _support/extract_glql_wasm.rb /tmp/glql.wasm
GLQL_NODE_MODULES may name either a checkout root or a node_modules
directory. With it unset, the script searches this repository, its parent, and a
sibling gitlab/ checkout, in that order.
Reference artifact, so a future reader can confirm they have the same module:
size 2028464 bytes
sha256 a2f4ac75cd698bdaef87e7e5bfb7426645ad961fdf5a6890b0378bbca67d848d
Keeping the module out of this repo is deliberate: extracting from the already-pinned npm package is what keeps the module the gateway runs identical to the one the frontend and Rails run. Vendoring a copy would add a fourth place a GLQL version can drift. When the frontend bundle stops shipping wasm, replace the script with a build-time fetch of the published artifact, or vendor a copy behind a checksum gate.
Running the tests
go build ./...
go vet ./...
# Full suite. GLQL_WASM_PATH points at an extracted module (see above).
GLQL_WASM_PATH=/tmp/glql.wasm go test ./...
The go directive is 1.25.8 rather than a bare 1.25 because
gitlab.com/gitlab-org/labkit declares that patch level and the module
graph will not resolve below it.
GLQL_WASM_PATH is the only setup the tests need. Without it, the transpiler
tests fall back to invoking _support/extract_glql_wasm.rb, and if that cannot
find the npm package they skip rather than fail — a checkout with no GitLab
tree next to it is not a broken build.
That fallback is a real coverage gap, not a pass. Verified counts:
| Run | Executed | Skipped |
|---|---|---|
GLQL_WASM_PATH set |
78 | 0 |
| no module available | 50 | 28 |
So treat a green run with 28 skips as "the transpiler is untested here". CI
prints the skip count for exactly this reason (see .gitlab-ci.yml). The
glqlwasm package takes ~2 minutes with the real module, as several tests drive
the 256 MiB memory ceiling on purpose.
License
MIT — see LICENSE.