A self-contained PostgreSQL CLI written in pure Rust. No libpq. No system libraries. One binary.
psql requires a full PostgreSQL client installation (libpq, system libraries, OS packages).
This creates friction in containers, CI pipelines, minimal Linux distros, macOS without Homebrew,
and Windows. pgcli-rs eliminates that friction: one binary, zero system dependencies,
cross-platform.
| Feature | psql | pgcli |
|---|---|---|
| Requires libpq | β Yes | β No |
| Single static binary | β No | β Yes |
| Windows native | β Yes | |
| SQL syntax highlighting | β No | β Yes |
| JSON / CSV / JSONL / TSV export | β No | β Yes |
| Deep schema introspection | β Yes | |
| DDL generation | β No | β Yes |
| Benchmark / profiling mode | β No | β Yes |
| SCRAM-SHA-256 auth (pure Rust) | β Yes | β Yes |
| musl static binary | β No | β Yes |
- π Zero dependencies - pure Rust wire protocol, no libpq, no system libs
- π¦ Single static binary - drop it anywhere and run (musl on Linux)
- π₯οΈ Cross-platform - Linux, macOS, Windows (x86_64 and ARM64)
- π Full auth support - SCRAM-SHA-256, MD5, Trust,
.pgpassfile - π TLS modes - disable / prefer / require / verify-full
- π¨ SQL syntax highlighting - real-time coloring in the REPL
- π Multi-line input - automatic continuation prompt for incomplete statements
- π Multiple output formats - table, csv, json, jsonl, tsv, html
- π€ Data export - INSERT statements, COPY format, file output
- π Deep introspection - system catalog queries for tables, indexes, functions, sequences
- π§Ύ DDL generation - reconstruct CREATE TABLE statements from live schema
- π Script execution -
.sqlfile runner with variable substitution - π Single-transaction mode - wrap script files in a transaction
- π Benchmark mode -
--repeat N+--statsfor latency histograms and throughput - π Persistent history - deduplication, configurable size
Download from the Releases page:
| Platform | Binary |
|---|---|
| Linux x86_64 | pgcli-rs-linux-x86_64.tar.gz |
| Linux ARM64 | pgcli-rs-linux-aarch64.tar.gz |
| macOS x86_64 | pgcli-rs-macos-x86_64.tar.gz |
| macOS ARM64 | pgcli-rs-macos-aarch64.tar.gz |
| Windows x86_64 | pgcli-rs-windows-x86_64.zip |
git clone https://github.com/jhonsferg/pgcli-rs
cd pgcli-rs
cargo build --release
./target/release/pgcli --versionFully static musl binary (Linux):
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
# Result: target/x86_64-unknown-linux-musl/release/pgcli (~7 MB, no dynamic deps)# Connect with individual flags
pgcli -h localhost -p 5432 -U postgres -d mydb
# Connect via URI
pgcli "postgresql://myuser:mypass@localhost:5432/mydb"
# Connect with TLS (require encrypted connection)
pgcli -h db.example.com -U postgres -d prod --require-tls
# Connect to a local socket (Unix domain socket)
pgcli -h /var/run/postgresql -U postgres -d mydbOnce connected you will see:
pgcli 0.1.0-connected to mydb at localhost:5432
Type \? for help. Type \q or press Ctrl-D to exit.
mydb=#
# Run a SQL query and exit
pgcli -h localhost -U postgres -c "SELECT version();"
# Run a meta-command (backslash command) and exit
pgcli -h localhost -U postgres -c "\l"
pgcli -h localhost -U postgres -d mydb -c "\dt"
# List all databases
pgcli -l -h localhost -U postgres# Table (default)
pgcli -h localhost -U postgres -d mydb -c "SELECT id, name, salary FROM employees LIMIT 5;"
# CSV-pipe-friendly
pgcli -h localhost -U postgres -d mydb --format csv \
-c "SELECT id, name, salary FROM employees LIMIT 5;"
# JSON array
pgcli -h localhost -U postgres -d mydb --format json \
-c "SELECT id, name FROM employees LIMIT 3;"
# JSON Lines (one object per line)
pgcli -h localhost -U postgres -d mydb --format jsonl \
-c "SELECT id, name FROM employees LIMIT 3;"
# Tab-separated
pgcli -h localhost -U postgres -d mydb --format tsv \
-c "SELECT id, name, salary FROM employees LIMIT 5;"
# HTML table
pgcli -h localhost -U postgres -d mydb --format html \
-c "SELECT * FROM products LIMIT 10;" -o report.html# Execute a SQL script
pgcli -f schema.sql -h localhost -U postgres -d mydb
# Execute as a single transaction (all-or-nothing)
pgcli -f migrations/0042_add_column.sql -h localhost -U postgres -d mydb \
--single-transaction# Run the same query 20 times and print a latency histogram
pgcli -h localhost -U postgres -d mydb --repeat 20 --stats \
-c "SELECT count(*), avg(salary) FROM employees;"
# Run 10 iterations without printing intermediate results (stats only)
pgcli -h localhost -U postgres -d mydb --repeat 10 --stats \
-c "SELECT * FROM orders WHERE status = 'delivered' LIMIT 1000;"Sample benchmark output:
-- Benchmark: 20 runs -----------------------------
Latency (ms) min=1.899 avg=2.360 max=3.612
Percentiles p50=2.180 p95=3.295 p99=3.612
Throughput 424 rows/sec
Data xfer 14.2 KB total (6017 KB/sec)
Total rows 20
Process RSS 5192 KB
------------------------------------------------
| Flag | Long form | Default | Description |
|---|---|---|---|
-h HOST |
--host |
localhost or $PGHOST |
Server hostname or IP address |
-p PORT |
--port |
5432 or $PGPORT |
Server TCP port |
-U USER |
--username |
$PGUSER or current OS user |
Database login user |
-d DBNAME |
--dbname |
$PGDATABASE or username |
Target database name |
-W |
--password |
- | Force interactive password prompt |
-w |
--no-password |
- | Never prompt for password; fail instead |
| Flag | Long form | Description |
|---|---|---|
-c SQL |
--command |
Execute single SQL statement or meta-command and exit |
-f FILE |
--file |
Execute a SQL script file and exit |
-o FILE |
--output |
Write output to FILE instead of stdout |
-l |
--list |
List all databases and exit |
-1 |
--single-transaction |
Wrap -f script in a single transaction |
| Flag | Long form | Description |
|---|---|---|
-t |
--tuples-only |
Print rows only; suppress headers and footers |
-A |
--no-align |
Unaligned output mode |
-H |
--html |
HTML <table> output |
-F SEP |
--field-separator |
Field separator for unaligned mode (default: |) |
-R SEP |
--record-separator |
Record separator (default: newline) |
-q |
--quiet |
Suppress informational messages |
-e |
--echo-queries |
Echo each query to stderr before executing |
| Flag | Description |
|---|---|
--no-tls |
Disable TLS completely (insecure) |
--require-tls |
Require TLS; fail if server does not support it |
--tls-ca FILE |
CA certificate file for server certificate verification |
--tls-cert FILE |
Client certificate file (mutual TLS) |
--tls-key FILE |
Client private key file (mutual TLS) |
| Flag | Default | Description |
|---|---|---|
--format FORMAT |
table |
Output format: table csv json jsonl tsv html |
--export FILE |
- | Save result to FILE (format inferred from extension) |
--timeout SECS |
30 |
Connection and query timeout in seconds |
--max-rows N |
1000 |
Max rows displayed in interactive mode |
--pager CMD |
less -RFX |
Override pager command |
--no-pager |
- | Disable pager output |
--theme THEME |
dark |
Color theme: dark light none |
--history-file FILE |
~/.pgcli-rs_history |
History file path |
--config FILE |
~/.config/pgcli-rs/config.toml |
Config file path |
--repeat N |
1 |
Repeat --command N times (benchmark mode) |
--stats |
- | Print latency histogram and throughput after execution |
| Command | Description |
|---|---|
\q, \quit |
Exit pgcli |
\c DBNAME [USER] |
Connect to a different database |
\conninfo |
Show current connection details |
\! CMD |
Execute a shell command |
\? , \help |
Show meta-command help |
| Command | Description |
|---|---|
\l [PATTERN], \list |
List databases |
\d [PATTERN] |
Describe tables, views, and sequences matching pattern |
\dt [PATTERN] |
List tables (schema.table patterns supported, e.g. \dt hr.*) |
\dv [PATTERN] |
List views |
\di [PATTERN] |
List indexes |
\ds [PATTERN] |
List sequences |
\df [PATTERN] |
List functions |
\dn [PATTERN] |
List schemas |
\du [PATTERN] |
List roles and permissions |
Pattern syntax: * matches anything, ? matches a single character, schema.name filters by both schema and object name.
-- List all tables in the "hr" schema
\dt hr.*
-- List tables whose names start with "ord"
\dt ord*
-- Describe the employees table
\d hr.employees| Command | Description |
|---|---|
\x [on|off] |
Toggle expanded (vertical) display mode |
\timing [on|off] |
Toggle query execution timing |
\format FORMAT |
Switch output format mid-session |
\theme THEME |
Switch color theme mid-session |
| Command | Description |
|---|---|
\size [TABLE] |
Show disk size of entire database or a specific table |
\locks |
Show active locks from pg_locks |
\activity |
Show active queries from pg_stat_activity |
\introspect TABLE |
Deep introspection of a table (columns, constraints, indexes) |
\ddl TABLE |
Generate CREATE TABLE DDL for a table |
| Command | Description |
|---|---|
\set [VAR VALUE] |
Set a session variable (or list all variables) |
\unset VAR |
Remove a session variable |
\echo TEXT |
Print text to stdout |
pgcli supports all common PostgreSQL authentication methods:
| Method | Description |
|---|---|
| Trust | No password required |
| Password (cleartext) | Simple password over the wire (use TLS!) |
| MD5 | Salted MD5 password hash |
| SCRAM-SHA-256 | Modern, secure challenge-response auth (default in PG14+) |
--password/-Wflag β interactive prompt$PGPASSWORDenvironment variable~/.pgpassfile (or$PGPASSFILE).pgpassrecord format:hostname:port:database:username:password
# ~/.pgpass example
db.example.com:5432:mydb:myuser:s3cr3t
*:5432:*:readonly:readonlypass
pgcli respects all standard PostgreSQL environment variables:
| Variable | Description |
|---|---|
PGHOST |
Server hostname |
PGPORT |
Server port |
PGUSER |
Database user |
PGPASSWORD |
Password (prefer ~/.pgpass for security) |
PGDATABASE |
Database name |
PGPASSFILE |
Path to .pgpass file |
PGSSLMODE |
TLS mode: disable allow prefer require verify-full |
PAGER |
Pager command (e.g. less -RFX, more) |
RUST_LOG |
Log level filter (e.g. RUST_LOG=info) |
Default location: ~/.config/pgcli-rs/config.toml
[connection]
host = "localhost"
port = 5432
timeout_secs = 30
[display]
theme = "dark" # dark | light | none
syntax_highlight = true
max_rows = 1000
border = 1
null_display = ""
expanded = "auto" # auto | on | off
[pager]
command = "less -RFX"
enabled = true
[history]
file = "~/.pgcli-rs_history"
max_entries = 10000
[output]
format = "table" # table | csv | json | jsonl | tsv | html
timing = truepgcli is built on a clean layered architecture with zero C dependencies:
main.rs
β-- CliArgs (cli/args.rs) - clap-derive argument parsing
β-- ConnectionConfig (connection/config.rs) - merge CLI + env + .pgpass
β-- TlsConfig (connection/tls.rs) - TLS mode handling
β-- ConnectionPool (connection/pool.rs) - single-connection pool + reconnect
β-- AuthHandler (protocol/auth.rs) - SCRAM-SHA-256 / MD5 / Trust
β-- [non-interactive] ScriptPipeline / QueryExecutor
β-- [interactive] ReplEditor
β-- MetaCommandDispatcher (meta/commands.rs)
β-- QueryExecutor (executor/query.rs)
β-- QueryResult (protocol/messages.rs)
β-- Formatter (output/formats.rs)
β-- Pager (output/pager.rs)
Key dependencies:
| Crate | Role |
|---|---|
tokio-postgres |
Pure-Rust PostgreSQL wire protocol |
tokio |
Async runtime |
rustls / native-tls |
TLS backends |
rustyline |
Readline/REPL input |
comfy-table |
Unicode table rendering |
clap |
CLI argument parsing |
tracing |
Structured logging |
Benchmarks measured against a PostgreSQL 16 server over LAN:
| Workload | Platform | p50 latency | p95 latency | Throughput |
|---|---|---|---|---|
| Simple aggregate (20 runs) | Linux (loopback) | 2.2 ms | 3.3 ms | 424 rows/sec |
| Simple aggregate (20 runs) | Windows (LAN) | 12.0 ms | 27.1 ms | 62 rows/sec |
| 1 000-row result (5 runs) | Linux (loopback) | 9.9 ms | 11.4 ms | 99 K rows/sec |
| 1 000-row result (5 runs) | Windows (LAN) | 23.4 ms | 32.2 ms | 41 K rows/sec |
| Complex JOIN + GROUP BY | Linux (loopback) | 14.3 ms | 17.2 ms | 476 rows/sec |
Memory footprint: < 10 MB RSS at peak for a 1 000-row result set.
# Unit tests - no database required
cargo test --lib
# Lint check
cargo clippy -- -D warnings
# Format check
cargo fmt --check
# Integration tests - requires a live PostgreSQL instance
PGCLI_RS_TEST_DSN="postgresql://user:pass@localhost/testdb" \
cargo test --features integration-tests
# Build documentation
cargo doc --no-deps --open# Prefer TLS (default) - upgrades if server supports it
pgcli -h db.example.com -U postgres -d mydb
# Require TLS - fail if server does not support it
pgcli -h db.example.com -U postgres -d mydb --require-tls
# Verify server certificate (verify-full)
pgcli -h db.example.com -U postgres -d mydb \
--require-tls --tls-ca /etc/ssl/certs/my-ca.crt
# Mutual TLS (client certificate authentication)
pgcli -h db.example.com -U postgres -d mydb \
--require-tls \
--tls-cert ~/.config/pgcli-rs/client.crt \
--tls-key ~/.config/pgcli-rs/client.key \
--tls-ca ~/.config/pgcli-rs/ca.crt
# Via environment variable
PGSSLMODE=require pgcli -h db.example.com -U postgres -d mydb# Export query result to CSV file
pgcli -h localhost -U postgres -d mydb \
-c "SELECT * FROM employees" \
--format csv -o employees.csv
# Export to JSON
pgcli -h localhost -U postgres -d mydb \
-c "SELECT id, name, salary FROM employees WHERE active = true" \
--format json -o active_employees.json
# Export as SQL INSERT statements
pgcli -h localhost -U postgres -d mydb \
-c "\export employees"
# Pipe CSV directly to another tool
pgcli -h localhost -U postgres -d mydb \
--format csv --tuples-only \
-c "SELECT name, salary FROM employees ORDER BY salary DESC LIMIT 100" \
| sort -t, -k2 -rn | head -10Because pgcli-rs is a single static binary, it is ideal for containers and CI pipelines:
# Multi-stage: copy the binary into a scratch container
FROM scratch
COPY --from=builder /workspace/pgcli /usr/local/bin/pgcli-rs# GitHub Actions example
- name: Run DB migration check
run: |
./pgcli -h ${{ secrets.DB_HOST }} \
-U ${{ secrets.DB_USER }} \
-d ${{ secrets.DB_NAME }} \
-f migrations/latest.sql \
--single-transaction
env:
PGPASSWORD: ${{ secrets.DB_PASSWORD }}See CONTRIBUTING.md for development setup, coding conventions, and the pull request process.
Coding rules at a glance:
- No
.unwrap()/.expect()outsidemain.rs - All
pubitems have///doc comments - Every module has a
#[cfg(test)]block cargo fmt+cargo clippy -- -D warningsmust pass- No libpq or C PostgreSQL bindings of any kind
- Use
tracing::error!/warn!/info!-nevereprintln!
MIT License. See LICENSE.
Built with π¦ Rust Β |Β Pure PostgreSQL wire protocol Β |Β Zero system dependencies
