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/qsqlTo 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 --helpQuick start
Open a local SQLite database (it’s created if it doesn’t exist) and you’re at an interactive prompt:
qsql mydata.dbConnected 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=> \qType \? 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 ServerquicSQL 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' # ed25519A 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 keyThe 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:
| Command | What it does |
|---|---|
\d, \dt, \di, \l | describe tables, indexes, list databases |
\d tablename | show a table’s columns, types, indexes |
\pset format csv|json|html|… | change the output format |
\g file / \g |cmd | send results to a file or a pipe |
\copy SRC DST "QUERY" TABLE | copy rows from one database into another |
\backup FILE | save a copy of the connected database locally |
\watch [secs] | re-run the last query on an interval |
\i file.sql | run commands from a file |
\if / \elif / \else / \endif | conditional scripting (see below) |
\q | quit |
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 inConditional 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
\endifOutput, 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 tagsquicSQL 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 sessionsThe 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 environmentThe 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/.