Skip to content
The qsql CLI

qsql

A fast, friendly command-line SQL client — a psql-grade shell in a single, dependency-free binary. It talks to local SQLite files, quicSQL servers (over HTTP/1.1, HTTP/2, HTTP/3, or Unix sockets), Postgres, MySQL, and SQL Server, and it ships schema-migration and code-generation tools built on LiteORM.

The same shell, DSN grammar, and \d commands work identically against a local file and a remote server — only the connection string changes.

Install

Homebrew (macOS and Linux)

brew install quicsql/tap/qsql

To upgrade later: brew upgrade qsql.

With Go

go install quicsql.net/qsql@latest   # needs Go 1.26+

Prebuilt binaries

Grab a static binary for your platform from the releases page, drop it on your PATH, and you’re done — there are no runtime dependencies.

From source

git clone https://github.com/quicsql/qsql && cd qsql
go build -o qsql .

Verify the install:

qsql --version
qsql --help

Quick start

Open a local SQLite database (it’s created if it doesn’t exist) and you’re at an interactive prompt:

qsql mydata.db
Connected with driver sqlite (SQLite 3.53.2 (gosqlite))
Type "help" for help.

sq:mydata.db=> CREATE TABLE fruit (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE
sq:mydata.db=> INSERT INTO fruit (name) VALUES ('apple'), ('pear');
INSERT 2
sq:mydata.db=> SELECT * FROM fruit;
 id | name
----+-------
  1 | apple
  2 | pear
(2 rows)
sq:mydata.db=> \q

Type \? for the list of backslash commands, \q to quit.

Connecting

Pass a connection string as the single argument. A bare path or .db file opens local SQLite; a URL opens the matching database:

qsql app.db                                                # local SQLite file
qsql sqlite::memory:                                       # throwaway in-memory database
qsql 'quicsql://host:7777/app?transport=h2&token=SECRET'   # quicSQL server (token over TLS)
qsql postgres://user:pw@host/dbname                        # PostgreSQL
qsql mysql://user:pw@host/dbname                           # MySQL
qsql sqlserver://user:pw@host/dbname                       # SQL Server

quicSQL transports and authentication

Pick a transport with ?transport= (h1 cleartext HTTP/1.1 is the default; also h2c, h2, h3, unix) and authenticate with a query parameter or a flag:

qsql 'quicsql://host:7777/app?transport=h2&token=SECRET'        # bearer over TLS
qsql 'quicsql://host:7777/app?transport=h2&user=alice&password=…' # HTTP basic over TLS
qsql 'quicsql://alice:pw@host:7777/app?transport=h2'            # HTTP basic (psql-style userinfo)
qsql --cert client.pem --key client.key 'quicsql://host:7777/app?transport=h2'   # mTLS
qsql --ed25519-key ~/.ssh/id_ed25519 'quicsql://host:7777/app?transport=h2'      # ed25519

A user:password@host userinfo (the familiar postgres://… shape) is HTTP Basic — the same as ?user=&password=. Bearer has no username, so a token always goes in ?token=, never in userinfo. Client certificates and ed25519 keys can’t be written in a URL, so they’re flags — and they work on every subcommand too.

By default qsql refuses to send a token or password over a cleartext transport (h1/h2c), so credentials aren’t leaked on the wire — use h2/h3 (TLS) or a Unix socket. A loopback host (127.0.0.1, ::1, localhost) is trusted automatically, since the credential never leaves your machine, so local development just works; for any other host over cleartext, opt in explicitly with allow_insecure_auth=1 in the connection string.

Encrypted SQLite databases

Add ?crypto to a local SQLite DSN to transparently encrypt the file at rest (gosqlite’s pure-Go encryption VFS — Adiantum by default, or ?crypto=aes-xts for AES-XTS-256):

qsql 'sqlite:secret.db?crypto'              # prompts for a passphrase
QSQL_SQLITE_PASSPHRASE=… qsql -c '…' 'sqlite:secret.db?crypto'
qsql --sqlite-passphrase-file pass.txt 'sqlite:secret.db?crypto'
qsql --sqlite-key-file key.hex 'sqlite:secret.db?crypto'   # raw 32/64-byte key

The passphrase or key is never taken from the DSN or the command line — qsql prompts (twice, when creating a new database), or reads it from QSQL_SQLITE_PASSPHRASE, --sqlite-passphrase-file, or a raw --sqlite-key-file (hex, base64, or binary; 32 bytes for Adiantum, 64 for AES-XTS). In passphrase mode the key is derived with Argon2id over a random salt stored in a secret.db.salt sidecar next to the database — keep it with the file; losing the salt (or the passphrase) makes the data unrecoverable. \backup of an encrypted database writes an encrypted copy.

This is confidentiality at rest only — it hides the file’s contents, but does not detect tampering; pair it with filesystem-level integrity if an attacker can write to the file.

Vault containers (compression and multi-recipient encryption)

?vault opens the database as a gosqlite vault container — a block-structured file that can be compressed, encrypted, both, or neither, independently:

qsql 'sqlite:app.db?vault'                       # plain container
qsql 'sqlite:app.db?vault&compress=best'         # compressed (see levels below)
qsql 'sqlite:app.db?vault&crypto&compress=best'  # compressed + encrypted (passphrase/key)

Compression is a named level, not a number: none, fastest, fast, default, better, best (fastest/fast are LZ4; default/better/best are zstd). A bare ?compress means default.

Encryption reuses the same key sources as ?crypto above (passphrase prompt, QSQL_SQLITE_PASSPHRASE, --sqlite-passphrase-file, or --sqlite-key-file). Alternatively, seal a container to one or more age recipients — each holder opens it with their own SSH key, no shared secret:

qsql --sqlite-recipient ~/.ssh/id_ed25519.pub 'sqlite:app.db?vault'   # create, sealed to a key
qsql --sqlite-identity  ~/.ssh/id_ed25519     'sqlite:app.db?vault'   # open with your key

--sqlite-recipient takes an SSH public-key line or an authorized_keys file (repeatable, for several recipients); --sqlite-identity takes an SSH private key. Passphrase/key mode and recipient mode are mutually exclusive.

Using the shell

Inside the prompt you get everything you’d expect from psql:

CommandWhat it does
\d, \dt, \di, \ldescribe tables, indexes, list databases
\d tablenameshow a table’s columns, types, indexes
\pset format csv|json|html|…change the output format
\g file / \g |cmdsend results to a file or a pipe
\copy SRC DST "QUERY" TABLEcopy rows from one database into another
\backup FILEsave a copy of the connected database locally
\watch [secs]re-run the last query on an interval
\i file.sqlrun commands from a file
\if / \elif / \else / \endifconditional scripting (see below)
\qquit

Run tab-completion on table and column names, up-arrow through history, and get syntax-highlighted input on a color terminal.

Running scripts and one-liners

qsql -c 'SELECT count(*) FROM fruit;' app.db      # run one command and exit
qsql -f setup.sql app.db                          # run a file and exit
echo 'SELECT 1;' | qsql sqlite::memory:           # pipe SQL in

Conditional scripting

\if / \elif / \else / \endif let a script branch on a condition — handy for idempotent setup. The condition is a boolean (on/off, true/false, 1/0) after variable and backtick substitution:

SELECT CASE WHEN count(*) = 0 THEN 'on' ELSE 'off' END AS need_setup
  FROM sqlite_master WHERE name = 'fruit';
\gset
\if :need_setup
  CREATE TABLE fruit (id INTEGER PRIMARY KEY, name TEXT);
  \echo created schema
\else
  \echo schema already present, skipping
\endif

Output, config, and files

Set the output format and other options with \pset (or the -A/-C/-J/-H flags for unaligned/CSV/JSON/HTML). Save named connections and defaults in config.yaml under ~/.config/qsql/ (honoring $XDG_CONFIG_HOME; the same path on Linux and macOS, %AppData%\qsql\ on Windows). Environment variables use the QSQL_ prefix; command history is ~/.qsql_history; a startup script runs from ~/.qsqlrc.

Name your connections and set a default so a bare qsql connects straight to it:

# ~/.config/qsql/config.yaml
dsn: local                       # default when qsql is run with no argument;
                                 # a URL or the name of a connection below
connections:
  local: "sqlite:app.db"
  prod:  "quicsql://db.example.com:7777/app?transport=h2&token=…"

Now qsql opens local, qsql prod opens the prod server, and a URL on the command line still overrides everything. The default can also come from the QSQL_DSN environment variable (which wins over the config file).

Credentials and security

Keep secrets out of your shell history: prefer the qsqlpass password file or the --ed25519-key / --cert flags over putting token=/password= in a connection string on the command line. qsql redacts credentials in \conninfo, error messages, and its own history file, but the argument you type is still recorded by your shell.

TLS certificate verification is on by default. --insecure (or ?insecure=1) turns it off for development and prints a warning; a pinned --ca bundle always wins over a stray ?insecure=1. As in psql, \!, backtick substitution, and \i/-f run shell commands and SQL, so only run script files you trust.

Schema migrations and code generation

qsql includes LiteORM’s migration runner and code generator, usable against any connection:

qsql migrate new init                        # create migrations/000001_init.{up,down}.sql
qsql migrate up   <connection>               # apply pending migrations
qsql migrate status <connection>             # show what's applied (also \migrate in the shell)
qsql migrate down <connection>               # roll back one step
qsql migrate version <connection>            # print the current migration version
qsql migrate force <connection> <version>    # set the version after fixing a failed migration

qsql gen models  <connection> [tables…]      # generate Go structs from a live schema
qsql gen queries queries.sql                 # generate typed Go from annotated SQL
qsql gen port    old_models.go               # convert gorm tags to LiteORM tags

quicSQL server operations

qsql ping   <url>                            # health check
qsql export <url> [file]                     # download a full database image
qsql admin databases|info|sessions <url>     # control plane (needs an admin login)
qsql admin create  <url> -f spec.json        # create a database at runtime
qsql admin detach|kill|maintenance …         # manage databases and sessions

The admin login is a bearer token (?token=…) or a key (--ed25519-key/ --cert). quicSQL bearer auth has no username, so user:token@… is not a token — a user:password@… URL is HTTP Basic (the password method) instead.

Managing one server without repeating its URL

The server URL on every ops command is optional: set your admin server as the default (or a named connection) and drop the URL entirely.

# ~/.config/qsql/config.yaml
dsn: prod-admin
connections:
  prod-admin: "quicsql://db.example.com:7777/app?transport=h2&token=OPS_TOKEN"
qsql admin databases                 # uses the default server
qsql admin sessions
qsql admin kill  <session-id>         # URL omitted; the id is the only argument
qsql admin maintenance app compact    # <database> <op>, default server
qsql admin databases staging          # or name another connection instead
export QSQL_DSN='quicsql://…?token=…'; qsql admin info   # or via the environment

The same views are also available inside the interactive shell as \admin commands — qsql (bare, using the default DSN) drops you into a shell where \admin databases, \admin sessions, etc. work against the connected server.

Migrating an existing database to quicSQL

qsql speaks both ends of the move, so no dump files are needed:

# 1. explore the source
qsql -c '\dt' postgres://user:pw@host/appdb

# 2. capture the schema as Go models (review and adjust types)
qsql gen models postgres://user:pw@host/appdb users orders -o models.go

# 3. rebuild the schema on the server
qsql migrate new init && $EDITOR migrations/000001_init.up.sql
qsql migrate up 'quicsql://server:7777/app?transport=h2&token=SECRET'

# 4. copy the data across, table by table
qsql -c '\copy postgres://user:pw@host/appdb quicsql://server:7777/app?transport=h2&token=SECRET "SELECT id, name FROM users" users'

# 5. verify
qsql -c 'SELECT count(*) FROM users;' 'quicsql://server:7777/app?transport=h2&token=SECRET'

The full walkthrough — type mapping, foreign-key ordering, and pitfalls — is in skills/migrate-to-quicsql/.