Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ferrule

The collar that joins you to your data.

Ferrule is a Rust-native command-line tool for querying relational databases. One static binary speaks PostgreSQL, MySQL, MSSQL, and SQLite. Oracle is optional via feature flag.

Features

  • Zero runtime dependencies for default backends (no libpq, libmysqlclient, or unixodbc required).
  • Unified type system — every backend maps native types to a single Value enum.
  • Multiple output formats — table, JSON, CSV, YAML, raw.
  • Interactive REPL with history, bookmarks, query parameters, and watch mode.
  • Schema introspectiontables and describe against any backend.
  • Server-side paging with --limit and --offset.
  • Connection registry — save named connections in ~/.config/ferrule/.
  • EXPLAIN execution plans across all backends.
  • Dump / Load data to/from CSV and JSON.
  • Watch mode for monitoring queries in real time.

One-minute Demo

# SQLite works out of the box
ferrule query "sqlite::memory:" "SELECT 1 + 1 AS answer;"

# Postgres (requires running server)
ferrule query "postgres://user:pass@localhost/db" "SELECT * FROM users;" --format json

# Interactive REPL
ferrule repl "sqlite::memory:"
> SELECT random() % 100 AS lucky_number;
> \format json
> SELECT * FROM sqlite_master;
> \q

Installation

From Source

Prerequisites:

  • Rust (stable toolchain)
  • For Oracle support: Oracle Instant Client libraries
git clone https://github.com/rustpunk/ferrule.git
cd ferrule

# Default build (Postgres, MySQL, MSSQL, SQLite)
cargo build --release --bin ferrule

# With Oracle support
cargo build --release --bin ferrule --features oracle

# Install to ~/.cargo/bin
cargo install --path ferrule-cli

The resulting binary is at ./target/release/ferrule.

Pre-built Binaries

Pre-built binaries are published on the releases page.

curl -L https://github.com/rustpunk/ferrule/releases/latest/download/ferrule-linux-x64.tar.gz | tar xz
sudo mv ferrule /usr/local/bin/

Feature Flags

FeatureDefaultDescription
postgresPostgreSQL backend (via tokio-postgres + rustls)
mysqlMySQL backend (via mysql_async)
mssqlMSSQL backend (via tiberius)
sqliteSQLite backend (via rusqlite, bundled)
oracleOracle backend (requires Instant Client)

To build a minimal binary with only SQLite:

cargo build --release --bin ferrule --no-default-features --features sqlite

Quick Start

First Query

# SQLite works without any setup
ferrule query "sqlite::memory:" "SELECT 1 + 1 AS answer;"

Output (TTY defaults to table format):

 answer
--------
 42

Save a Named Connection

Typing full URLs repeatedly is tedious. Save the ones you use often:

ferrule conn add production "postgres://user@db.example.com/app"

# Now use the name instead of the full URL
ferrule query production "SELECT * FROM customers LIMIT 5;"
ferrule tables production
ferrule repl production

Pipe-Friendly Defaults

When stdout is not a TTY, output defaults to JSON:

ferrule query "sqlite::memory:" "SELECT 1" | jq '.[]."1"'
# > 1

Save a Bookmark

For queries you run all the time:

ferrule bookmark add daily-count "SELECT COUNT(*) FROM events;" --connection production

ferrule bookmark run daily-count

Password Resolution

Ferrule resolves passwords via a stack:

  1. --password CLI flag
  2. FERRULE_<NAME>_PASSWORD environment variable
  3. OS keyring (keyring://ferrule/<name>)
  4. Interactive prompt (TTY only)
  5. Fail with diagnostic
# Set via environment
FERRULE_PRODUCTION_PASSWORD=secret ferrule query production "SELECT 1;"

# Store in OS keyring
ferrule conn set-password production

JSON Output with Paging

ferrule query production "SELECT * FROM events" \
  --format json --limit 50 --offset 100

File Output

ferrule query production "SELECT * FROM events" --output events.json

Explore Schema

# List tables
ferrule tables production

# Describe a table
ferrule describe production events

Connections

Raw Connection URLs

Pass a database URL directly as the first argument to any query, tables, describe, dump, watch command:

# SQLite (bundled — no external server)
ferrule query "sqlite::memory:" "SELECT 1;"
ferrule query "sqlite:///home/me/data/logs.db" "SELECT COUNT(*) FROM events;"

# PostgreSQL
ferrule query "postgres://ferrule:ferrule@localhost:5432/myapp?sslmode=disable" "SELECT * FROM users;"

# MySQL
ferrule query "mysql://root:pass@localhost:3306/myapp" "SHOW TABLES;"

# MSSQL  
ferrule query "mssql://sa:pass@127.0.0.1:1433/myapp?trustServerCertificate=true" "SELECT 1;"

URL Formats

BackendURL PatternNotes
PostgreSQLpostgres://user:pass@host:port/db?sslmode=disablesslmode values: prefer, require, disable
MySQLmysql://user:pass@host:port/db
MSSQLmssql://user:pass@host:port/db?trustServerCertificate=trueSet trustServerCertificate=true for self-signed certs
SQLitesqlite:///absolute/path or sqlite::memory::memory: does not work after ://; use the form above
Oracleoracle://user:pass@host:port/service_nameRequires --features oracle

URL Safety

Passwords are redacted in all logs, error messages, and verbose diagnostics:

ferrule query --verbose "postgres://user:secret@host/db" "SELECT 1"
# [ferrule] Resolved URL: postgres://user:***@host/db

Saving Named Connections

Typing full URLs repeatedly is tedious and leaks passwords to shell history. Save named connections:

# Production database
ferrule conn add production "postgres://app@prod.example.com/myapp"

# Local development copy
ferrule conn add dev "postgres://localhost:5432/myapp_dev"

# A read-only replica
ferrule conn add replica "postgres://readonly@replica.internal/myapp"

# Analytics warehouse on a different port
ferrule conn add warehouse "postgres://analytics@warehouse.internal:8432/events"

# Local SQLite file
ferrule conn add local "sqlite:///home/me/projects/myapp/dev.db"

Using Named Connections

Anywhere a connection URL is expected, you can substitute the saved name:

# Query using a name
ferrule query production "SELECT COUNT(*) FROM orders;"

# List tables
ferrule tables dev

# Describe a table
ferrule describe warehouse events

# Start the REPL on a saved connection
ferrule repl local

# Watch a production metric
ferrule watch replica "SELECT COUNT(*) FROM replication_lag;"

Managing the Registry

# List all saved connections
ferrule conn list
# production  => postgres://app@prod.example.com/myapp
# dev         => postgres://localhost:5432/myapp_dev
# replica     => postgres://readonly@replica.internal/myapp
# warehouse   => postgres://analytics@warehouse.internal:8432/events
# local       => sqlite:///home/me/projects/myapp/dev.db

# Test connectivity
ferrule conn test production
# Connection 'production' is alive.

# Remove an entry
ferrule conn remove warehouse

Where Connections Are Stored

~/.config/ferrule/connections.toml

Format:

[production]
name = "production"
url = "postgres://app@prod.example.com/myapp"

[dev]
name = "dev"
url = "postgres://localhost:5432/myapp_dev"

Password Resolution Stack

When a saved URL does not contain a password, Ferrule resolves one automatically in this order:

1. Explicit --password flag

ferrule query production "SELECT * FROM users;" --password "my-secret"

2. Per-connection environment variable

export FERRULE_PRODUCTION_PASSWORD="my-secret"
ferrule query production "SELECT * FROM users;"

The variable name is derived from the connection name:

  • productionFERRULE_PRODUCTION_PASSWORD
  • local-dbFERRULE_LOCAL_DB_PASSWORD

3. OS Keyring

# Store password without putting it on disk
ferrule conn set-password production
# Password: ••••••••

# Remove from keyring
ferrule conn delete-password production

Passwords are stored as service=ferrule, account=<name> in the OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service).

4. Interactive prompt (TTY only)

$ ferrule query production "SELECT * FROM users;"
Password for 'production': ••••••••

5. Fail with diagnostic

If all four steps fail, Ferrule exits with code 2:

ferrule::connection
  × Could not resolve password for 'production'
  ├─ No --password flag
  ├─ FERRULE_PRODUCTION_PASSWORD is unset
  ├─ keyring://ferrule/production: not found
  └─ Terminal is not interactive

Project-Local Profiles

For team-shared settings, add a .ferrule.toml to your project root:

[default]
format = "json"
limit = 100

[connection.production]
url = "postgres://app@${DB_HOST:-db.example.com}/myapp"

Ferrule discovers .ferrule.toml automatically. The URL above uses optional environment interpolation — if DB_HOST is unset, it falls back to db.example.com.

Named connections from the registry take precedence over .ferrule.toml profiles if they share a name.

Querying Data

Basic Queries

ferrule query "sqlite::memory:" "SELECT 1 + 1 AS answer;"

Output Formats

FormatFlagBest For
Table--format tableHuman-readable terminal output
JSON--format jsonPiping to jq or APIs
CSV--format csvSpreadsheets, data pipelines
YAML--format yamlHuman-readable structured data
Raw--format rawSimple columnar text

Default: table when TTY, json when piped.

Paging

Server-side LIMIT / OFFSET is injected when you pass --limit and --offset:

# Postgres, MySQL, SQLite
ferrule query db "SELECT * FROM users" --limit 25 --offset 50

# MSSQL uses OFFSET/FETCH syntax automatically
ferrule query mssql_db "SELECT * FROM users" --limit 25 --offset 50

Pass --limit 0 to disable paging entirely (useful for multi-statement batches).

Multi-Statement Batches

Backends that support it (Postgres, MSSQL) allow multiple statements separated by semicolons:

ferrule query production "
  INSERT INTO logs (msg) VALUES ('startup');
  SELECT COUNT(*) FROM logs;
"

⚠️ Multi-statement batches do not support --limit / --offset.

Parameterized Queries

Use ${name} placeholders in SQL and pass values via --param:

ferrule query production \
  'SELECT * FROM events WHERE severity = ${sev} AND created_at > ${date}' \
  --param "sev=error" \
  --param "date=2025-01-01"

Types are inferred automatically:

  • true / false → boolean
  • -42 / 3.14 → numeric
  • Anything else → string (properly quoted)

Load many parameters from JSON:

ferrule query production 'SELECT * FROM users WHERE id = ${id}' \
  --param-file params.json

Where params.json is:

{"id": 42, "name": "Alice"}

Dry Run

Preview the substituted SQL without executing:

ferrule query production 'SELECT * FROM users WHERE id = ${id}' \
  --param "id=42" --dry-run

Schema Introspection

List Tables

ferrule tables production
ferrule tables production --format json

Output:

[
  {"table_name": "users"},
  {"table_name": "events"},
  {"table_name": "schema_migrations"}
]

Describe Table

ferrule describe production users

Output:

[
  {
    "column_name": "id",
    "data_type": "integer",
    "is_nullable": "NO",
    "column_default": "nextval('users_id_seq'::regclass)"
  },
  {
    "column_name": "email",
    "data_type": "character varying(255)",
    "is_nullable": "NO"
  }
]

Note: Column default and nullable information varies by backend capability.

Bookmarks

Bookmarks let you save frequently-run queries so you don’t have to retype them (or maintain a separate SQL script directory).

Saving Bookmarks

# Save a simple query
ferrule bookmark add active-users "SELECT * FROM users WHERE active = true;" --connection dev

# Save with positional parameter placeholders
ferrule bookmark add user-by-id "SELECT * FROM users WHERE id = ${1};" --connection dev

# Long query — wrap in quotes
ferrule bookmark add recent-sales '
SELECT product, SUM(amount) as total
FROM orders
WHERE created_at > now() - interval '"'"'7 days'"'"'
GROUP BY product
ORDER BY total DESC;
' --connection production

The --connection hint is optional but recommended — without it, bookmark run will require a --connection flag every time.

Naming Convention

Plain names work:

ferrule bookmark add count-all "SELECT COUNT(*) FROM ${1};"

Dotted names are treated as connection hints — the first segment suggests the connection to use:

# pg.select_users → auto-uses connection named "pg"
ferrule bookmark add pg.select_users "SELECT id, name, email FROM users;"

# When a dotted name doesn't match a saved connection, Ferrule falls back
# to requiring --connection or the default profile

Listing Bookmarks

ferrule bookmark list

Output:

Name             | SQL
--------------------------------------------------------------
active-users     | SELECT * FROM users WHERE active = true;
user-by-id       | SELECT * FROM users WHERE id = ${1};
recent-sales     | SELECT product, SUM(amount) as total FR...
pg.select_users  | SELECT id, name, email FROM users;

Running Bookmarks

# Run a simple bookmark
ferrule bookmark run active-users

# Run a bookmark with positional parameters
ferrule bookmark run user-by-id 42

# Run with a different format than the global default
ferrule bookmark run recent-sales --format table

# Override the suggested connection
ferrule bookmark run pg.select_users --connection staging

# Combine with output paging
ferrule bookmark run active-users --limit 10 --offset 20

Parameter substitution replaces ${1}, ${2}, etc. with the provided arguments. Missing parameters leave the placeholder intact.

Deleting Bookmarks

ferrule bookmark delete user-by-id

Where Bookmarks Are Stored

~/.config/ferrule/bookmarks.toml

Format:

[active-users]
sql = "SELECT * FROM users WHERE active = true;"
connection = "dev"

[user-by-id]
sql = "SELECT * FROM users WHERE id = ${1};"
connection = "dev"

Workflow Example

Bookmarks shine in day-to-day workflows. Here’s a typical pattern:

# Morning standup — quick metrics
ferrule bookmark run daily-metrics

# On-call alert — replication lag
ferrule bookmark run check-lag

# Customer support — lookup by email
ferrule bookmark run user-by-email 'alice@example.com'

# End of sprint — export user growth chart
ferrule bookmark run user-growth --format csv > sprint_growth.csv

Using Bookmarks in the REPL

Bookmarks also work interactively from within the REPL:

> SELECT * FROM users WHERE active = true;
> \bookmark save active-users
Bookmark 'active-users' saved.

> \bookmark list
- active-users
- daily-metrics

> \bookmark run active-users
> \bookmark delete active-users

REPL bookmarks are saved to the same ~/.config/ferrule/bookmarks.toml file as CLI bookmarks.

Interactive REPL

Launch the REPL with any connection:

ferrule repl "sqlite::memory:"
ferrule repl production

Features

  • Readline editing via rustyline (arrow keys, history, reverse search).
  • Multi-line SQL — statements spanning multiple lines are collected until a trailing ;.
  • History persisted to ~/.cache/ferrule/history.
  • Session parameters (\param) and bookmarks (\bookmark).
  • Watch mode (\watch) for live-query monitoring without leaving the REPL.

Meta-Commands

Prefix with \:

CommandDescription
\qQuit REPL
\conn [name]Switch connection or show current
\d [table]Describe table (list tables if no arg)
\dt [schema]List tables
\format [fmt]Set output format
\limit [N]Set row limit (0 to clear)
\timing [on|off]Toggle timing display
\verbose [on|off]Toggle verbose logging
\param <name> <value>Set session parameter
\param clearClear all parameters
\param listList parameters
\bookmark save <name>Save last SQL as bookmark
\bookmark listList bookmarks
\bookmark run <name>Run a bookmark
\bookmark delete <name>Delete a bookmark
\explain <sql>Explain a query
\explainToggle explain mode
\watch [sql]Watch a query (re-executes every 5s)
\dump <table>Dump table to stdout
\load <file> INTO <table>Load data from a file
\helpShow help

Bookmarks

Bookmarks are saved to ~/.config/ferrule/bookmarks.toml.

> SELECT * FROM users WHERE active = true;
> \bookmark save active-users
Bookmark 'active-users' saved.

> \bookmark list
- active-users

> \bookmark run active-users

Bookmarks support positional parameter substitution with ${1}, ${2}, etc:

[by-id]
sql = "SELECT * FROM users WHERE id = ${1};"
connection = "production"
> \bookmark run by-id 42

Watch Mode

Watch the last query or a new one directly from the REPL:

> SELECT COUNT(*) FROM events;
> \watch              # watches the COUNT(*) query
> \watch interval 3   # change interval
> \watch stop         # stop background watch

Watch prints a header on each iteration and supports --diff mode (only show output when it changes).

Advanced Features

EXPLAIN

Get execution plans from any backend:

# Postgres: returns JSON plan
ferrule explain production "SELECT * FROM large_table WHERE status = 'pending'"

# MSSQL: returns graphical plan
ferrule explain mssql_db "SELECT * FROM orders WHERE customer_id = @p1"

# In the REPL, \explain toggles plan mode for every subsequent query
> \explain
Explain mode: on
> SELECT * FROM users;
# shows plan instead of results

Dump and Load

Dump

Export a table to CSV, JSON, or SQL INSERT statements:

ferrule dump production users --dump-format csv > users.csv
ferrule dump production users --dump-format json > users.json
ferrule dump production users --dump-format sql > users.sql

Dump is batched using server-side paging and works on large tables.

Load

Import CSV or JSON into a table:

ferrule load production users.json --table users --create-table
ferrule load production data.csv --table events

Load infers CSV columns from the first row and JSON schema from object keys. With --create-table, Ferrule generates a CREATE TABLE statement using inferred types.

Watch Mode

Monitor a query repeatedly with clean screen handling and diff support:

# Basic watch (re-runs every 5 seconds)
ferrule watch production "SELECT COUNT(*) FROM events" --interval 5

# Only print when result changes
ferrule watch production "SELECT COUNT(*) FROM events" --interval 2 --diff

# Limit iterations
ferrule watch production "SELECT NOW()" --interval 1 --max-iterations 10

Ctrl-C stops the watch cleanly.

Configuration

Discovery Order

Ferrule loads configuration from (first found wins):

  1. --config <path> CLI flag
  2. ./.ferrule.toml (project-local)
  3. ~/.config/ferrule/ferrule.toml (platform-appropriate)

Example .ferrule.toml

[default]
format = "json"
limit = 1000

[connection.production]
url = "postgres://user@db.example.com/app"

[connection.staging]
url = "mysql://user@staging.internal/app"

Environment Interpolation

Profile URLs can reference environment variables with ${VAR}:

[connection.production]
url = "postgres://user@${DB_HOST}/app"

Per-Profile Defaults

[connection.readonly]
url = "postgres://readonly@replica.example.com/app"
format = "table"   # override global default for this profile
limit = 50         # narrower default paging

Backends

PostgreSQL

  • Pure Rust via tokio-postgres + rustls (no OpenSSL)
  • Supports SSL modes: prefer, require, disable
  • Multi-statement batches supported
  • UUID, JSONB, arrays mapped to Ferrule Value types
ferrule query "postgres://user:pass@host/db?sslmode=require" "SELECT 1;"

MySQL

  • Pure Rust via mysql_async
  • Works with MySQL 5.7+ / MariaDB 10.3+
  • JSON column type mapped to Value::Json
ferrule query "mysql://root:pass@127.0.0.1:3306/mydb" "SELECT * FROM users;"

MSSQL

  • Pure Rust via tiberius
  • Supports Windows Authentication (Kerberos) and SQL Authentication
  • DATETIMEOFFSET mapped to Value::DateTimeTz
  • Bit columns mapped to Value::Bool
ferrule query "mssql://sa:pass@host/db?trustServerCertificate=true" "SELECT 1;"

SQLite

  • Statically linked via rusqlite with bundled feature
  • No runtime library required
  • In-memory databases via sqlite::memory:
ferrule query "sqlite::memory:" "SELECT 1;"
ferrule query "sqlite:///tmp/mydb.sqlite3" "SELECT * FROM users;"

Oracle

Optional feature requiring Oracle Instant Client on the host at runtime.

cargo build --release --features oracle

ferrule query "oracle://user:pass@host:1521/service" "SELECT * FROM dual;"

If Instant Client is missing, Ferrule emits a diagnostic with download links.

Type Mapping Summary

Ferrule ValuePostgresMySQLMSSQLSQLiteOracle
BoolBOOLEANBOOLEANBITINTEGERNUMBER(1)
Int64BIGINTBIGINTBIGINTINTEGERNUMBER
Float64DOUBLEDOUBLEFLOATREALBINARY_FLOAT
DecimalNUMERICDECIMALDECIMALNUMERICNUMBER
StringTEXTVARCHARNVARCHARTEXTVARCHAR2
BytesBYTEABLOBVARBINARYBLOBRAW
DateDATEDATEDATETEXTDATE
DateTimeTIMESTAMPDATETIMEDATETIME2TEXTTIMESTAMP
DateTimeTzTIMESTAMPTZTIMESTAMPDATETIMEOFFSETTEXTTIMESTAMP WITH TIME ZONE
JsonJSONBJSONNVARCHAR(MAX)TEXTCLOB
UuidUUIDCHAR(36)UNIQUEIDENTIFIERTEXTRAW(16)

Reference

Exit Codes

CodeMeaning
0Success
1Usage error (invalid CLI arguments, malformed SQL)
2Connection error (network, auth, backend unavailable)
3Query error (syntax error, constraint violation)
4No rows returned (optional, used by --expect-rows)

Environment Variables

VariablePurpose
FERRULE_<NAME>_PASSWORDPassword for connection <NAME>
FERRULE_CONFIGPath to config file override
RUST_LOGEnable debug logging from underlying crates

CLI Quick Reference

Connections

ferrule conn add <name> <url>
ferrule conn list
ferrule conn remove <name>
ferrule conn test <name>
ferrule conn set-password <name>
ferrule conn delete-password <name>

Querying

ferrule query <connection> <sql> [options]
ferrule explain <connection> <sql>
ferrule tables <connection>
ferrule describe <connection> <table>

Bookmarks

ferrule bookmark add <name> <sql> [--connection <conn>]
ferrule bookmark list
ferrule bookmark run <name> [param1] [param2] ... [--connection <conn>] [--format <fmt>]
ferrule bookmark delete <name>

Bookmark names can be dotted (pg.select_users) — the first segment suggests the connection to use.

Data Movement

ferrule dump <connection> <table> [--dump-format <fmt>] [--file <path>] [--schema <schema>]
ferrule load <connection> <file> --table <table> [--create-table]

Monitoring

ferrule watch <connection> <sql> [--interval <secs>] [--diff] [--max-iterations <N>]

Interactive

ferrule repl <connection>

Common Options (for query, tables, describe, dump, explain, watch)

FlagDescription
-f, --format <fmt>Output format: table, json, csv, yaml, raw
-n, --limit <N>Server-side row limit (0 to disable)
--offset <N>Skip N rows
--timingShow timing diagnostics
-v, --verboseShow resolved URL and SQL
--insecureDisable TLS verification
-p, --password <pwd>Explicit password
--output <FILE>Write results to a file
--daemonRoute through connection-pooling daemon

File Locations

FilePurposePath
Global configPer-user defaults, connection profiles~/.config/ferrule/ferrule.toml
ConnectionsSaved name → URL registry~/.config/ferrule/connections.toml
BookmarksSaved query library~/.config/ferrule/bookmarks.toml
HistoryREPL command history~/.cache/ferrule/history

Type Reference

Every backend maps native types to a unified Value enum:

Ferrule ValuePostgresMySQLMSSQLSQLiteOracle
BoolBOOLEANBOOLEANBITINTEGERNUMBER(1)
Int64BIGINTBIGINTBIGINTINTEGERNUMBER
Float64DOUBLEDOUBLEFLOATREALBINARY_FLOAT
DecimalNUMERICDECIMALDECIMALNUMERICNUMBER
StringTEXTVARCHARNVARCHARTEXTVARCHAR2
BytesBYTEABLOBVARBINARYBLOBRAW
DateDATEDATEDATETEXTDATE
DateTimeTIMESTAMPDATETIMEDATETIME2TEXTTIMESTAMP
DateTimeTzTIMESTAMPTZTIMESTAMPDATETIMEOFFSETTEXTTIMESTAMP WITH TIME ZONE
JsonJSONBJSONNVARCHAR(MAX)TEXTCLOB
UuidUUIDCHAR(36)UNIQUEIDENTIFIERTEXTRAW(16)