Plan Runner
This crate implements an executor for (conjunctive) query plans.
The implementation is a CLI tool.
It reads a JSON plan (which currently is a DAG of scan and join nodes plus the input facts),
walks the DAG using the operators from query-ops,
and prints the resulting relation as JSON to stdout.
Pipeline
End-to-end, scenarios become runner output through three stages:
tools/exporter/examples/*.scenario.json
└── (Haskell exporter; runs Geolog.DB.Plan.planConjunction
and Geolog.DB.InMemory.evalConjunctionPlanned as a self-check)
└── crates/plan-runner/fixtures/*.json (JSON IR; checked in)
└── (plan-runner; this crate)
└── stdout JSON, with row-for-row oracle check
The exporter (tools/exporter) is the only producer of runner IR today;
it's where atoms are planned and rejected if they don't fit the supported subset.
Fixtures are regenerated with make export-fixtures, and the full loop is make examples.
What happens inside the runner once a JSON plan arrives:
Storage Backends
The CLI takes a --backend flag.
The memory backend is the pure in-memory path;
every other backend routes facts through the Storage trait
via build_tables_via_storage, then scans tables back out before executing.
| Backend | Storage | Location |
|---|---|---|
memory |
none | n/a |
memory-storage |
MemoryStorage |
in-process |
lmdb |
LmdbStorage |
fresh tempdir per run |
redb |
RedbStorage |
fresh tempdir per run |
fjall |
FjallStorage |
fresh tempdir per run |
sqlite |
SqliteStorage |
fresh tempdir per run |
geomerge |
GeomergeStorage |
in-process |
⚠️
--backend geomergerequires a typed theory upfront, but the runner IR is untyped. The CLI infers column types (PrimIntorPrimString) from the first fact row per relation; relations with no facts default toPrimString. Works for every current fixture; future fixtures with mixed-type columns may fail at insert time.
Execute a Query Plan
# Run a plan with the default backend (no storage)
cargo run -p plan-runner -- crates/plan-runner/fixtures/two_atom_join.json
# Run the same plan with every supported backend
cargo run -p plan-runner -- --backend memory-storage crates/plan-runner/fixtures/two_atom_join.json
cargo run -p plan-runner -- --backend lmdb crates/plan-runner/fixtures/two_atom_join.json
cargo run -p plan-runner -- --backend redb crates/plan-runner/fixtures/two_atom_join.json
cargo run -p plan-runner -- --backend fjall crates/plan-runner/fixtures/two_atom_join.json
cargo run -p plan-runner -- --backend sqlite crates/plan-runner/fixtures/two_atom_join.json
cargo run -p plan-runner -- --backend geomerge crates/plan-runner/fixtures/two_atom_join.json
A sample run:
$ plan-run crates/plan-runner/fixtures/two_atom_join.json
{"columns":["a","b","_w0_2"],"rows":[["node:1","node:2","edge:1"],["node:2","node:1","edge:2"]]}
The _w<atomIdx>_<pos> columns are wildcards the exporter named so the runner can bind them.
The scenario's expected_bindings block names only the variables the test cares about,
and verify projects the runner output to that subset before comparing as a multiset.
Run the Tests
cargo test -p plan-runner
Notes
- IR contract.
The runner is backend-agnostic and frontend-agnostic.
It consumes JSON in the shape documented in
src/lib.rsand produces a binding relation. Anything that emits the same JSON can drive it. - No optimizer. Plans are executed as written. Node ordering, join shape, and antijoin scheduling are all the producer's responsibility. This crate's job ends at faithful execution of the IR.
- Wildcard columns survive.
scan_atomkeeps every distinct variable that appears in the pattern, including the exporter's synthetic_w<atomIdx>_<pos>names. The runner does not project them out; oracle verification handles that on the comparison side. - Bulk, not streaming.
Each node materializes its full output as a
Relation. This matchesquery-ops' execution model; it's not designed for incremental or maintained-view workloads.