> ## 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), 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 ...
```
