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

# Query

> Run and submit queries through the oleander query router.

The SDK has two query methods and one legacy one. `query_run` reads, `query_submit` writes, and both go through the [query router](/platform/query-routing/overview), which picks the engine and machine size for you. All methods are async.

## `query_run(sql_or_options)`

Runs a query and returns the rows on the call.

oleander parses the SQL, estimates how much data the referenced tables hold, and picks the engine (`duckdb`, `polars`, `bloom`) and machine size to match, so leave `engine` as `auto` unless you want a specific one. The choice and the reasoning behind it come back in `engine_decision`.

```python theme={null}
from oleander_sdk import Oleander

oleander = Oleander()

result = await oleander.query_run(
    "SELECT * FROM oleander.default.flowers LIMIT 10",
)

print(result.results.columns, result.results.rows)
print(result.row_count, result.execution_time)
print(result.engine_decision.engine, result.engine_decision.reasons)
```

`query_run` is read-only. SQL that could change data is rejected before the request goes out - use `query_submit` for those.

### Explain without running

Pass `explain=True` to see which engine a query would take, its estimated input size, and whether your plan allows it, without running anything or spending compute.

```python theme={null}
from oleander_sdk import QueryRunOptions

result = await oleander.query_run(
    QueryRunOptions(sql="SELECT * FROM oleander.default.events", explain=True)
)

print(result.engine_decision.engine, result.engine_decision.size_band)
```

Do this before a query you expect to be large.

### Polars scripts

Pass `script` instead of `sql` to run a Polars DataFrame script that assigns `result`, listing the tables it reads in `tables`. This forces the Polars engine.

```python theme={null}
from oleander_sdk import QueryRunOptions, QueryTable

scripted = await oleander.query_run(
    QueryRunOptions(
        script="result = events.group_by('day').len()",
        tables=[QueryTable(alias="events", table="default.events")],
    )
)
```

### Parameters

<ParamField body="sql" type="str">
  The SQL query to run. Pass a bare string as the only argument for the common case, or a `QueryRunOptions` instance for anything else. Mutually exclusive with `script`.
</ParamField>

<ParamField body="script" type="str">
  A Polars DataFrame script that assigns `result`. Requires `tables`.
</ParamField>

<ParamField body="tables" type="list[QueryTable]">
  Tables the script reads, as `QueryTable(alias=..., table=...)`.
</ParamField>

<ParamField body="engine" type="&#x22;auto&#x22; | &#x22;duckdb&#x22; | &#x22;polars&#x22; | &#x22;bloom&#x22;" default="&#x22;auto&#x22;">
  Ask for a specific engine. An impossible combination raises an engine capability error rather than rerouting.
</ParamField>

<ParamField body="explain" type="bool" default="False">
  Return the routing decision without executing.
</ParamField>

## `query_submit(options)`

Covers everything that changes data: a `SELECT` plus a `destination` to write it to, and a statement that names its own target (`INSERT`, `UPDATE`, `DELETE`, `MERGE`, DDL) with no destination. It also takes reads too large to return interactively.

Whether the write finishes on the call depends on the engine the router picked, so read `state`. Result rows are never returned, only `row_count` when it is known.

```python theme={null}
from oleander_sdk import QuerySubmitOptions

submitted = await oleander.query_submit(
    QuerySubmitOptions(
        sql="SELECT day, count(*) AS n FROM oleander.default.events GROUP BY day",
        destination="default.daily_counts",
        write_mode="overwrite",  # or "append"
    )
)

print(submitted.state, submitted.output_table, submitted.run_id)
```

| `state` | Meaning |
| - | - |
| `COMPLETE` | The write already landed. Nothing to poll. |
| `SUBMITTED` | A job is running. `run_id` is returned and the write is not visible yet. |

### `query_submit_and_wait(options)`

Submits and polls until an asynchronous run finishes. A write that lands inline returns immediately with `run` unset.

```python theme={null}
from oleander_sdk import QuerySubmitAndWaitOptions

waited = await oleander.query_submit_and_wait(
    QuerySubmitAndWaitOptions(
        sql="INSERT INTO oleander.default.daily_counts SELECT * FROM staging.daily",
    )
)

print(waited.state)  # COMPLETE | FAIL | ABORT
```

### Parameters

<ParamField body="sql" type="str">
  The statement to run. With a `destination`, a `SELECT`; without one, a statement that names its own target.
</ParamField>

<ParamField body="destination" type="str">
  Table to write the result to, as `namespace.table`.
</ParamField>

<ParamField body="write_mode" type="&#x22;overwrite&#x22; | &#x22;append&#x22;" default="&#x22;overwrite&#x22;">
  Applies to a `destination` write. A statement that names its own target carries its own semantics.
</ParamField>

<ParamField body="engine" type="&#x22;auto&#x22; | &#x22;duckdb&#x22; | &#x22;polars&#x22; | &#x22;bloom&#x22; | &#x22;spark&#x22;" default="&#x22;auto&#x22;">
  Ask for a specific engine.
</ParamField>

<ParamField body="explain" type="bool" default="False">
  Return the routing decision without submitting anything.
</ParamField>

## `query(sql, options?)`

<Warning>
  `query` predates the router. It always runs DuckDB, sized by the org's default sandbox setting rather than by the query. Prefer `query_run`; reach for this only when you need `save=True`.
</Warning>

Executes SQL against the lake on DuckDB, with optional auto-save by query hash. If the API rejects the query, the SDK raises instead of returning a failed result.

```python theme={null}
from oleander_sdk import QueryOptions

result = await oleander.query(
    "SELECT * FROM oleander.default.flowers LIMIT 10",
    QueryOptions(save=True),
)

if result.saved_table_name:
    print("Saved to:", result.saved_table_name)
```

### Parameters

<ParamField body="sql" type="str" required>
  The SQL query to execute. Supports DuckDB SQL syntax.
</ParamField>

<ParamField body="options.save" type="bool" default="False">
  When `True`, persists query results as a table. The table name is returned in `saved_table_name`.
</ParamField>

## Return fields

| Field | Type | Description |
| - | - | - |
| `success` | `bool` | Whether the query executed successfully |
| `results` | `QueryResultColumns` | `columns`, `column_types`, and `rows`. Reads only. |
| `row_count` | `Optional[int]` | Number of rows returned or written |
| `execution_time` | `Optional[str]` | Execution time, for example `"42ms"` |
| `engine_decision` | `Optional[EngineDecision]` | `engine`, `reasons`, `size_band`, `sandbox`. `query_run` and `query_submit` only. |
| `compute` | `Optional[Compute]` | The sandbox that actually served the run - the machine the org is billed for |
| `state` | `Optional[str]` | `COMPLETE` or `SUBMITTED`. `query_submit` only. |
| `run_id` | `Optional[str]` | Run to poll when `state` is `SUBMITTED` |
| `output_table` | `Optional[str]` | Destination table for a write |
| `saved_table_name` | `Optional[str]` | Table name if `save=True` was used on `query` |

## Iterating over results

```python theme={null}
result = await oleander.query_run(
    "SELECT species, sepal_length FROM oleander.default.flowers LIMIT 10"
)

columns = result.results.columns
rows = result.results.rows
species_idx = columns.index("species")
sepal_length_idx = columns.index("sepal_length")

for row in rows:
    species = row[species_idx]
    sepal_length = row[sepal_length_idx]
    # process each record ...
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.