How Ferrule Thinks
Ferrule is built around a few small abstractions. Once you have them, the rest of the docs make sense at a glance. This chapter is the conceptual “map” of the tool — read it once and refer back as needed.
The unified Value type
Every supported backend speaks a different SQL dialect with different
native types: PostgreSQL has JSONB and UUID, MySQL has its own JSON,
MSSQL has UNIQUEIDENTIFIER and DATETIMEOFFSET, SQLite has dynamic
typing, Oracle has NUMBER(1) standing in for booleans. If you wrote a
query against each one, you’d get five different shapes back.
Ferrule maps every backend’s native types into a single Value enum:
Null | Bool | Int64 | Float64 | Decimal | String | Bytes
| Date | Time | DateTime | DateTimeTz | Json | Uuid | Array
Output formatters (table, json, csv, yaml, raw) only ever see
Value — they never touch driver-specific types. The practical
consequence: a JSON dump from PostgreSQL and a JSON dump from MySQL look
the same. A CSV from MSSQL pipes into the same awk script as a CSV from
SQLite. You can switch backends without rewriting downstream tooling.
The exact backend → Value mapping is documented in
Backends. When a native type doesn’t have a clean Value
equivalent (Postgres int4range, custom enums, composite types),
ferrule falls back to String with the value rendered as the driver
returns it.
Connection resolution
Most ferrule commands take a <CONNECTION> argument. That argument is
resolved in three steps:
- Raw URL. If
<CONNECTION>parses as a database URL (postgres://...,mysql://...,sqlite::memory:, etc.), ferrule uses it directly. - Profile from
.ferrule.toml. If not a URL, ferrule looks up the name in the loaded[connection.<name>]profile of the discovered config file. Profiles can declare apassword_urland other defaults. - Registry from
connections.toml. If no matching profile exists, ferrule falls back to the per-user registry at~/.config/ferrule/connections.toml(the file managed byferrule conn add/list/remove).
If none of those match, ferrule exits with a usage error and lists the available profiles so you can spot a typo.
Note: profiles take precedence over the registry. If you have both a registry entry named
prodand a[connection.prod]block in.ferrule.toml, the profile wins. This lets a project-local.ferrule.tomlshadow a global registry entry without modifying it.
The credential stack
Once ferrule has a URL, it needs a password. URLs that include a password
inline (postgres://user:secret@host/db) skip this entirely — but
inline passwords leak through process listings and shell history, so
keep them out of saved profiles. See Security for the
full rationale.
When the URL has no password, ferrule walks the hasp credential stack in order. The first source that returns a value wins:
--passwordCLI flag. Explicit, ephemeral, and intentionally loud. Useful for one-off debugging; bad for daily use.password_urlfrom the active profile. Whatever thepassword_urlin.ferrule.tomlresolves to, via the hasp URL scheme —env://,keyring://, orfile://.FERRULE_<NAME>_PASSWORDenv var. Legacy fallback.production→FERRULE_PRODUCTION_PASSWORD. Hyphens become underscores:local-db→FERRULE_LOCAL_DB_PASSWORD.- OS keyring at
keyring://ferrule/<name>. Whateverferrule conn set-password <name>stored. macOS Keychain, Windows Credential Manager, or libsecret on Linux. - Interactive prompt. Only fires if stdin is a TTY.
- Fail. Exit code 2 with a diagnostic listing every step it tried.
This is the canonical home for the credential stack. The Connections and Security chapters expand on the why of each step.
Output format selection
Ferrule supports five output formats: table, json, csv, yaml,
raw. The format is chosen in this order:
--format <fmt>on the command line.default.formatin the loaded.ferrule.toml.- The built-in default, which is
json.
The built-in default is JSON whether or not stdout is a TTY. If you
want pretty tables in your terminal, either pass --format table per
command or set format = "table" under [default] in your config.
When writing to a file with --output, the format applies to the file
contents.
Multi-statement semantics
Some backends (PostgreSQL, MSSQL) accept multiple ;-separated
statements in a single round-trip. Ferrule supports this for those
backends:
ferrule query prod "
INSERT INTO logs (msg) VALUES ('startup');
SELECT COUNT(*) FROM logs;
"
A few rules:
--limitand--offsetare not allowed with multi-statement SQL. The default limit (1000from[default]) counts. Set--limit 0to disable paging for batches.- Each result is reported separately. Result sets print to stdout;
DML row counts (“
N rows affected”) print to stderr. - Backends that don’t support batches (MySQL, SQLite, Oracle in
the current driver layer) reject the SQL — split it into separate
ferrule querycalls or wrap it in a stored procedure.
Where to next
- New users: Quick Start for a guided first query.
- Curating connections: Connections.
- Picking the right secret store: Security.
- Looking up a flag: CLI Reference.
- Things going wrong: Troubleshooting.