Install Query Runner
Deploy Query Runner next to your warehouse and reload its config.
Updated
Query Runner is a binary that runs SQL on your warehouse for the engine. It stores warehouse credentials locally. Passwords never leave your infrastructure.
Install
Run the public image. It works on amd64 and arm64:
mkdir -p logs && sudo chown -R 65532:65532 logs
docker run -d \
--name query-runner \
-p 8443:8443 \
-e WAREHOUSE_DSN="postgres://user:pass@host:5432/analytics?sslmode=require" \
-v $(pwd)/config.yaml:/etc/query-runner/config.yaml:ro \
-v $(pwd)/logs:/var/log/query-runner \
ghcr.io/datahashi/query-runner:0.5.2
Pin an exact version such as 0.5.2 in production. 0.5 follows patch releases. latest follows every release.
The runner runs as uid 65532, not root. Any folder it writes to must belong to that uid. That is why the chown comes first.
Verify the image
Each image is signed. Check it with cosign:
cosign verify ghcr.io/datahashi/query-runner:0.5.2 \
--certificate-identity-regexp '^https://github.com/datahashi/query-runner/.github/workflows/release.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Upgrade from v0.4 or earlier
Older images ran as root, so your log folder may belong to root. The runner then stops at boot with permission denied and names the fix. Run it, then start again:
sudo chown -R 65532:65532 logs
Configure (config.yaml)
Create /etc/query-runner/config.yaml:
listen: ":8443"
admin_listen: "127.0.0.1:9090"
# Operational guardrails
policy:
enabled: true
statement_timeout: 60s
concurrency: 16
max_rows: 50000
max_stream_rows: 10000000
stream_timeout: 10m
# Warehouses managed by this runner
warehouses:
main_warehouse:
engine: postgresql
dsn_env: WAREHOUSE_DSN # postgres://user:pass@host:5432/analytics?sslmode=require
schemas: [public]
# Physical table/field allow-deny rules
policies:
analytics_policy:
default: deny
tables:
public.orders:
effect: allow
fields:
allow: [id, customer_id, amount, status, order_date]
deny: [internal_cost, vendor_margin]
values:
deny:
- { field: status, op: eq, value: deleted }
public.customers:
effect: allow
fields:
allow: [id, region, signup_date]
deny: [ssn, credit_card, password_hash]
# Runner authentication keys
# Generate token: openssl rand -hex 32
# Hash token: echo -n "<token>" | shasum -a 256
keys:
- id: engine_prod
sha256: "4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a"
policy: analytics_policy
sources: [main_warehouse]
# Local JSONL audit log
log:
path: /var/log/query-runner/queries.jsonl
include_sql: true
Warehouse DSN formats
Set credentials with environment variables:
| Warehouse | Driver format | Example DSN |
|---|---|---|
| PostgreSQL | Standard DSN | postgres://user:pass@host:5432/dbname?sslmode=require |
| MySQL | Go MySQL driver | user:pass@tcp(host:3306)/dbname?parseTime=true |
| Snowflake | Account locator DSN | user:pass@account_locator/dbname/schema?warehouse=wh |
| BigQuery and the other engines | Driver connection string in dsn_env |
The runner passes the string through. |
PostgreSQL is the only engine that enforces the tables rules above. Every other engine needs allow_unpoliced_backends: true.
Operate
Reload configuration
Update config.yaml. Then reload. The process stays up. A bad file keeps the previous snapshot. Pools for warehouses that left the snapshot are dropped.
docker kill -s HUP query-runner
# Without Docker:
kill -HUP $(pgrep query-runner)
# Or call local admin endpoint:
curl -X POST http://127.0.0.1:9090/admin/v1/reload
Pause all queries
Pause incoming queries at once:
curl -X POST http://127.0.0.1:9090/admin/v1/pause
To resume:
curl -X POST http://127.0.0.1:9090/admin/v1/resume
Connect to datahashi
When the engine can reach Query Runner:
-
In the console, go to Settings → Connections → New Connection.
-
Set Kind to
runner. -
Enter the Query Runner URL (for example
https://runner.internal:8443). -
Enter the raw unhashed runner secret key.
-
In the semantic model header, name the Connection:
source: main_warehouse
Next: Self-host the community engine · REST API.