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, orunixodbcrequired). - Unified type system — every backend maps native types to a single
Valueenum. - Multiple output formats — table, JSON, CSV, YAML, raw.
- Interactive REPL with history, bookmarks, query parameters, and watch mode.
- Schema introspection —
tablesanddescribeagainst any backend. - Server-side paging with
--limitand--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
| Feature | Default | Description |
|---|---|---|
postgres | ✅ | PostgreSQL backend (via tokio-postgres + rustls) |
mysql | ✅ | MySQL backend (via mysql_async) |
mssql | ✅ | MSSQL backend (via tiberius) |
sqlite | ✅ | SQLite backend (via rusqlite, bundled) |
oracle | ❌ | Oracle 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:
--passwordCLI flagFERRULE_<NAME>_PASSWORDenvironment variable- OS keyring (
keyring://ferrule/<name>) - Interactive prompt (TTY only)
- 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
| Backend | URL Pattern | Notes |
|---|---|---|
| PostgreSQL | postgres://user:pass@host:port/db?sslmode=disable | sslmode values: prefer, require, disable |
| MySQL | mysql://user:pass@host:port/db | |
| MSSQL | mssql://user:pass@host:port/db?trustServerCertificate=true | Set trustServerCertificate=true for self-signed certs |
| SQLite | sqlite:///absolute/path or sqlite::memory: | :memory: does not work after ://; use the form above |
| Oracle | oracle://user:pass@host:port/service_name | Requires --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:
production→FERRULE_PRODUCTION_PASSWORDlocal-db→FERRULE_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
| Format | Flag | Best For |
|---|---|---|
| Table | --format table | Human-readable terminal output |
| JSON | --format json | Piping to jq or APIs |
| CSV | --format csv | Spreadsheets, data pipelines |
| YAML | --format yaml | Human-readable structured data |
| Raw | --format raw | Simple 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 \:
| Command | Description |
|---|---|
\q | Quit 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 clear | Clear all parameters |
\param list | List parameters |
\bookmark save <name> | Save last SQL as bookmark |
\bookmark list | List bookmarks |
\bookmark run <name> | Run a bookmark |
\bookmark delete <name> | Delete a bookmark |
\explain <sql> | Explain a query |
\explain | Toggle 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 |
\help | Show 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):
--config <path>CLI flag./.ferrule.toml(project-local)~/.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
Valuetypes
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
DATETIMEOFFSETmapped toValue::DateTimeTz- Bit columns mapped to
Value::Bool
ferrule query "mssql://sa:pass@host/db?trustServerCertificate=true" "SELECT 1;"
SQLite
- Statically linked via
rusqlitewithbundledfeature - 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 Value | Postgres | MySQL | MSSQL | SQLite | Oracle |
|---|---|---|---|---|---|
Bool | BOOLEAN | BOOLEAN | BIT | INTEGER | NUMBER(1) |
Int64 | BIGINT | BIGINT | BIGINT | INTEGER | NUMBER |
Float64 | DOUBLE | DOUBLE | FLOAT | REAL | BINARY_FLOAT |
Decimal | NUMERIC | DECIMAL | DECIMAL | NUMERIC | NUMBER |
String | TEXT | VARCHAR | NVARCHAR | TEXT | VARCHAR2 |
Bytes | BYTEA | BLOB | VARBINARY | BLOB | RAW |
Date | DATE | DATE | DATE | TEXT | DATE |
DateTime | TIMESTAMP | DATETIME | DATETIME2 | TEXT | TIMESTAMP |
DateTimeTz | TIMESTAMPTZ | TIMESTAMP | DATETIMEOFFSET | TEXT | TIMESTAMP WITH TIME ZONE |
Json | JSONB | JSON | NVARCHAR(MAX) | TEXT | CLOB |
Uuid | UUID | CHAR(36) | UNIQUEIDENTIFIER | TEXT | RAW(16) |
Reference
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Usage error (invalid CLI arguments, malformed SQL) |
| 2 | Connection error (network, auth, backend unavailable) |
| 3 | Query error (syntax error, constraint violation) |
| 4 | No rows returned (optional, used by --expect-rows) |
Environment Variables
| Variable | Purpose |
|---|---|
FERRULE_<NAME>_PASSWORD | Password for connection <NAME> |
FERRULE_CONFIG | Path to config file override |
RUST_LOG | Enable 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)
| Flag | Description |
|---|---|
-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 |
--timing | Show timing diagnostics |
-v, --verbose | Show resolved URL and SQL |
--insecure | Disable TLS verification |
-p, --password <pwd> | Explicit password |
--output <FILE> | Write results to a file |
--daemon | Route through connection-pooling daemon |
File Locations
| File | Purpose | Path |
|---|---|---|
| Global config | Per-user defaults, connection profiles | ~/.config/ferrule/ferrule.toml |
| Connections | Saved name → URL registry | ~/.config/ferrule/connections.toml |
| Bookmarks | Saved query library | ~/.config/ferrule/bookmarks.toml |
| History | REPL command history | ~/.cache/ferrule/history |
Type Reference
Every backend maps native types to a unified Value enum:
Ferrule Value | Postgres | MySQL | MSSQL | SQLite | Oracle |
|---|---|---|---|---|---|
Bool | BOOLEAN | BOOLEAN | BIT | INTEGER | NUMBER(1) |
Int64 | BIGINT | BIGINT | BIGINT | INTEGER | NUMBER |
Float64 | DOUBLE | DOUBLE | FLOAT | REAL | BINARY_FLOAT |
Decimal | NUMERIC | DECIMAL | DECIMAL | NUMERIC | NUMBER |
String | TEXT | VARCHAR | NVARCHAR | TEXT | VARCHAR2 |
Bytes | BYTEA | BLOB | VARBINARY | BLOB | RAW |
Date | DATE | DATE | DATE | TEXT | DATE |
DateTime | TIMESTAMP | DATETIME | DATETIME2 | TEXT | TIMESTAMP |
DateTimeTz | TIMESTAMPTZ | TIMESTAMP | DATETIMEOFFSET | TEXT | TIMESTAMP WITH TIME ZONE |
Json | JSONB | JSON | NVARCHAR(MAX) | TEXT | CLOB |
Uuid | UUID | CHAR(36) | UNIQUEIDENTIFIER | TEXT | RAW(16) |