Our engine never sees your warehouse password
datahashi

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:

  1. In the console, go to Settings → Connections → New Connection.

  2. Set Kind to runner.

  3. Enter the Query Runner URL (for example https://runner.internal:8443).

  4. Enter the raw unhashed runner secret key.

  5. In the semantic model header, name the Connection:

    source: main_warehouse

Next: Self-host the community engine · REST API.