engine_decision, so a run is never a black box.
What each engine can do
Set
engine explicitly and oleander validates the request instead of rerouting - an impossible combination returns an engine capability error. Leave it on auto unless you have a reason.
When SQL isn’t enough
The router covers queries. Two engines take arbitrary code when the work is ingest, streaming, or a transformation that SQL cannot express.Spark
Serverless Spark runs PySpark applications and JARs you upload as versioned artifacts, on managed infrastructure or your own registered cluster. Reach for it when you need to:- Ingest from a source the lake does not attach - a JDBC database, an API, a proprietary format. Postgres imports are exactly this, packaged.
- Stream with Spark Structured Streaming, monitoring run events as batches arrive.
- Write arbitrary code - ML training, multi-step pipelines, anything with control flow.
Polars scripts
Passscript instead of sql and you get a Python Polars DataFrame program against your lake tables, in an isolated sandbox with no infrastructure to provision. Declare what it reads in tables, assign result, and the router sends it to Polars.
Lineage across engines
Every engine emits OpenLineage through the same path, so lineage is continuous no matter what ran a given step. A DuckDB query, a Bloom scan, a Polars script, and a Spark job that read and wrote the same tables produce the same dataset identities and connect into one graph. Dataset names are qualified with the session catalog specifically so they match across engines -oleander.default.events is the same node whichever engine touched it. Each event also carries the routing decision as a facet, so the graph records not just what ran but which engine ran it and why.
That is what makes a query and a job you wrote yourself equivalent to impact analysis and column lineage: the graph does not care which engine produced an edge.
Explain before you run
Passexplain: true to get the engine, estimated input size, and sandbox tier without executing anything or spending compute. Worth doing before a query you expect to be large.
Machine sizing
Roughly 2 GiB of input per vCPU, snapped up to a provisioned tier, with 2 GB of memory per vCPU.
Past 50 GiB the query leaves the sandbox and goes distributed, where worker count and machine type come from the same estimate:
Worker count is clamped to between 2 and 10, so 900 GiB caps at 10 ×
bloom.4.b.
A size you request explicitly wins outright. The org’s default sandbox size in lake settings still applies to the CLI and the SDKs’ legacy query methods, which bypass the router.
Responses report the sandbox that actually served the run under compute - a warm larger sandbox can serve a smaller request, and that is the machine the org is billed for.
Reading the decision
engine_decision carries engine, the ordered reasons trail, size_band, and sandbox. In the lake UI it is the tooltip on the engine badge; distributed and async runs return no rows and report through a toast with the engine, destination table, and a link to the run.
A 402 or 403 on a query is a billing decision, not a transient failure. Read the message and act on it rather than retrying the same query.