# The qp command-line client

> File a query request from a terminal or CI, wait for approval and download the masked result with qp, the command-line client for the QueryProxy REST API.

- Documentation: Guides
- Source: https://queryproxy.com/docs/cli/
- Language: en-US
- Author: Muhammet Şafak

---
`qp` is a command-line client for the [REST API](/docs/api/). It files a query
request, waits until a reviewer approves it and the worker has run it, and
downloads the result. It is one static binary with nothing to install around it,
so it fits a terminal as well as a CI job. It lives in its own repository,
[`QueryProxy/qp`](https://github.com/QueryProxy/qp).

`qp` can do only what the API allows. It cannot approve a request, and in
particular it cannot approve its owner's own request or override anything.
Nothing runs until a reviewer approves it, exactly as for a request filed in the
web UI, and the [SQL guards](/docs/sql-guards/) and masking apply as usual.

## Install

Download the archive for your platform from the
[releases page](https://github.com/QueryProxy/qp/releases). Binaries exist for
Linux, macOS and Windows, on amd64 and arm64. Put `qp` on your `PATH`.

The release workflow signs the checksum file keylessly with
[cosign](https://github.com/sigstore/cosign). Verify the download before you use
it:

```bash
cosign verify-blob \
  --bundle checksums.txt.sigstore.json \
  --certificate-identity-regexp '^https://github.com/QueryProxy/qp/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  checksums.txt
sha256sum --ignore-missing -c checksums.txt
```

## Authenticate

Create a [personal access token](/docs/api/#personal-access-tokens) in the web
UI (**Profile**). A token has scopes and an expiry; give it only what the
pipeline needs:

| To use | The token needs |
| :-- | :-- |
| `qp submit` | `requests:submit` (this also covers looking up a connection by name) |
| `qp submit --wait` | additionally `requests:read`, and `results:read` for the result |
| `qp status` | `requests:read` |
| `qp results` | `results:read`, plus `requests:read` for `--format json` |

```bash
export QP_URL=https://queryproxy.example.com
export QP_TOKEN='12|qp_...'
```

`--url` and `--token` override the environment. Prefer the environment: a flag
value shows up in the process list. `qp` never prints the token and scrubs it
from error messages. A plain `http://` URL works, with a warning unless it points
at the local machine, but sends the token unencrypted. `qp` does not follow
redirects, so set `QP_URL` to the final address.

## Commands

### qp submit

```bash
qp submit --conn <id|name> --file report.sql --reason "monthly report" --wait
```

| Flag | Meaning |
| :-- | :-- |
| `--conn` | Connection id, or its exact name (a number is always taken as an id) |
| `--file` | File with the SQL; `-` reads standard input |
| `--reason` | Why the statement is needed; the approver sees it |
| `--title` | Short title, optional |
| `--wait` | Wait for the approval and the run, then print the result |
| `--timeout` | With `--wait`: give up after this long. Default `30m`; `0` waits forever |
| `--poll-interval` | With `--wait`: how often to ask the server. Default `3s` |
| `--format`, `--output` | With `--wait`: as for `qp results` |

Progress goes to standard error. Without `--wait`, standard output carries only
the request id, so `id=$(qp submit ...)` works. With `--wait`, it carries only the
result. A timeout or Ctrl-C stops the waiting but does not cancel the request:
it stays on the server, and `qp status <id>` follows it.

### qp status

`qp status <id>` prints the request, as JSON with `--json`. For a finished
request it exits with the same codes as `qp submit --wait`.

### qp results

`qp results <id>` downloads the stored result of a completed request, already
masked by the connection's masking rules.

- `--format csv` (the default) writes the result as the server sends it.
- `--format json` writes one array of objects keyed by column name, streamed.
  Values keep their exact text; duplicate column names give duplicate keys.
- `--output <file>` writes to a file instead of standard output. The file gets
  mode `0600`, also when it already exists.

If the server cut the result at its row ceiling, `qp` says so on standard error.

`qp version` prints the version.

## Exit codes

| Code | Meaning |
| :-- | :-- |
| `0` | Completed successfully |
| `1` | Usage, configuration, network or API error: a bad token, an unknown connection, SQL the guard refused |
| `2` | The request was rejected |
| `3` | The request failed to run, or was cancelled |
| `4` | `--timeout` ran out before the request finished |
| `130` | Interrupted |

## In a pipeline

```yaml
- run: qp submit --conn prod-reporting --file sql/report.sql --reason "nightly report $GITHUB_RUN_ID" --wait --timeout 2h --output report.csv
  env:
    QP_URL: ${{ vars.QP_URL }}
    QP_TOKEN: ${{ secrets.QP_TOKEN }}
```

The job stays blocked until a reviewer decides, so give `--timeout` a value that
fits how fast your team approves, and use a token that expires soon.

## Next steps

- [REST API and personal access tokens](/docs/api/) for the endpoints behind `qp`.
- [Approval workflow](/docs/approval-workflow/) for who approves and how.
- [Production hardening](/docs/production-hardening/) for token hygiene.
