# Read-only ClickHouse Users (/docs/reference/readonly-users)

Give hypequery a dedicated ClickHouse user that can only read. ClickHouse has two
ways to make a user read-only, and hypequery accepts both. They differ in one
thing: whether hypequery can still apply its own query settings.

## The two levels [#the-two-levels]

| | `readonly = 1` | `readonly = 2` |
| --- | --- | --- |
| Blocks writes and schema changes | Yes | Yes |
| Accepts per-query settings | Only when the value matches the user's profile | Yes, except `readonly` itself |
| Limits hypequery can apply | Only what the profile already sets | All of them |

Both keep your data equally safe. The difference is who controls query limits:

- With `readonly = 1`, only the user's profile does. Nobody, hypequery included,
  can change a setting for a single query.
- With `readonly = 2`, hypequery can tighten limits per query. To stop anyone
  raising a limit past what you allow, add
  [constraints](https://clickhouse.com/docs/operations/settings/constraints-on-settings)
  to the profile, for example a `max` on `max_memory_usage`.

Use `readonly = 2` unless you have a reason not to. If you use `readonly = 1`,
put the limits you want in the user's profile.

## TypeScript query builder [#typescript-query-builder]

`@hypequery/clickhouse` sends one setting by default,
`output_format_json_quote_64bit_integers = 1`, so `Int64` and wider values arrive
as exact strings.

- `readonly = 2` and unrestricted users work without changes.
- A `readonly = 1` user works without changes if its profile already sets
  `output_format_json_quote_64bit_integers = 1`.
- Otherwise, set `integerJsonEncoding: 'server-default'`. Wide integers may then
  lose precision beyond `2^53`. See
  [64-bit integer encoding](/docs/reference/clickhouse-behavior#64-bit-integer-encoding-and-strict-read-only-users).

Any setting you pass yourself with `.settings({...})` follows the same rule: a
`readonly = 1` user rejects it unless the value already matches the profile.

### Arrow streams [#arrow-streams]

`streamArrow()` and `rawArrowStream()` request the format with a
`FORMAT ArrowStream` clause in the query, which needs no setting. They also send
`output_format_arrow_compression_method = 'none'` by default, because
JavaScript Arrow readers can't decode the LZ4 batches ClickHouse sends otherwise.

- `readonly = 2` and unrestricted users work without changes.
- A `readonly = 1` user works without changes if its profile already sets
  `output_format_arrow_compression_method = 'none'`.
- Otherwise, set `arrowCompression: 'server-default'` to omit the setting.
  Batches then use the server's compression method (`lz4_frame` by default),
  which suits readers with an LZ4 codec, such as `pyarrow`, but not
  JavaScript Arrow readers.

## Python datasets [#python-datasets]

The Python planner attaches `readonly = 1`, `max_execution_time`,
`max_result_rows`, `max_result_bytes`, and `max_threads` to every query. The
executor never silently drops a query limit.

- **`readonly = 2`:** works without configuration. The user may not change
  `readonly`, so when the driver reports that the user is already read-only
  the executor leaves out only the planner's `readonly` setting. Every query
  limit still applies.
- **`readonly = 1`:** set the same limits in the user's profile. If a limit
  differs, ClickHouse rejects the query; change the profile rather than
  disabling the limit in HypeQuery.
- **Unrestricted users** get all five settings, including `readonly = 1`.

To require a read-only user, set `readonly_policy="profile"` on the
connection. The executor then refuses, before any query is sent, a user whose
profile does not enforce `readonly = 1` or `2`:

<CodeBlock>
```python
from hypequery.execution import ClickHouseConnection, create_clickhouse_executor

executor = create_clickhouse_executor(
    ClickHouseConnection(
        host="localhost",
        username="hypequery_reader",
        password="...",
        readonly_policy="profile",
    )
)
```
</CodeBlock>

The same connection option works with `create_async_clickhouse_executor`.

A readonly mismatch, like ClickHouse rejecting the connection's credentials or
grants, is the server's fault, not the caller's. The executor reports it as
`unavailable`, so `hypequery.serve` answers `503` with a fixed message and
writes the reason, with the request id, to the `hypequery.serve` log.

Cancellation and deadlines still stop queries on the server for both levels,
because the executor cancels with `KILL QUERY`, which read-only users may run.
