DBIx::Fast
DBI, fast & easy — one small API over SQLite, MariaDB, MySQL and PostgreSQL.
DBIx::Fast sits between a bare DBI handle and a full ORM. You keep writing SQL — but the everyday plumbing (connection resilience, transactions, result shaping, caching, profiling and identifier safety) is handled for you, the same way across every supported driver.
It is not an ORM: no result classes, no relationships, no migrations. Just DBI with the sharp edges filed off.
use DBIx::Fast;
my $db = DBIx::Fast->new( dsn => 'postgres://user:pass@localhost:5432/app' );
my $user = $db->hash('SELECT * FROM users WHERE id = ?', $id);
my @names = $db->flat('SELECT name FROM users WHERE active = ?', 1);
my $total = $db->count('users', { status => { '>=' => 1 } });
$db->insert('users', { name => 'Alice', status => 1 }, time => 'created_at');
$db->up('users', { status => 2 }, { id => $id });
$db->txn(sub {
$db->insert('orders', { total => 100 });
$db->insert('order_items', { order_id => $db->last_id, sku => 'X' });
});
Why DBIx::Fast?
Most projects end up re-implementing the same boring, easy-to-get-wrong infrastructure around DBI. DBIx::Fast bundles it:
- Multi-driver, dialect-aware. One API over SQLite, MariaDB, MySQL and
PostgreSQL. Identifier quoting,
upsertsyntax, last-insert id, isolation-level SQL and schema introspection all adapt to the driver. - Resilient connections. Fork-safe handles, lazy connect, throttled
server-side
pingto catch dropped connections, automatic reconnect — and a guard that fails loudly instead of silently committing if a connection is lost mid-transaction. - Transactions that retry.
txnruns a block with automatic deadlock / lock-wait retry (detected by driver error code), nested transactions, named savepoints and per-transaction isolation levels. - Result caching. Optional in-process (or CHI-backed) cache with TTL and tag invalidation; CRUD writes auto-invalidate the affected table.
- Query profiling. Built-in tracker (timings, slow-query detection, stats by
type) plus driver-native diagnostics like
EXPLAINand index/connection analysis. - Security-conscious. Values always use placeholders; identifiers are
validated and quoted;
countoperators, isolation levels and savepoint names are whitelisted; error messages can optionally redact quoted literals (PII/PCI). - Modern & light. Built with Object::Pad (Perl 5.38+). Tiny core; the profiler and cache are lazily loaded only if used.
Quick start
# --- Connect (DSN, URI, or SQLite shortcut) ---
my $db = DBIx::Fast->new( dsn => 'dbi:MariaDB:dbname=app;host=localhost',
user => 'root', password => 'secret' );
my $db = DBIx::Fast->new( dsn => 'mariadb://root:secret@localhost:3306/app' );
my $db = DBIx::Fast->new( SQLite => '/path/to/app.db' );
# --- Read in the shape you want ---
my $rows = $db->all('SELECT * FROM users WHERE age > ?', 18); # arrayref of hashrefs
my $row = $db->hash('SELECT * FROM users WHERE id = ?', $id); # single hashref
my $name = $db->val('SELECT name FROM users WHERE id = ?', $id);# scalar
my @ids = $db->flat('SELECT id FROM users'); # flat list
# Memory-efficient iterator (does not slurp the whole result set)
my $rs = $db->query('SELECT * FROM logs ORDER BY id DESC');
while (my $log = $rs->hash) { ... }
# Named parameters
$db->execute('SELECT * FROM users WHERE name = :name', { name => 'Alice' });
# --- Write (driver-native upsert / bulk) ---
$db->upsert('products', { sku => 'X', stock => 5 }, ['sku'], ['stock']);
$db->insert_many('logs', \@rows);
# --- Transactions with retry, savepoints, isolation ---
$db->txn(sub {
$db->insert('accounts', { id => 1, balance => 100 });
$db->transaction->savepoint('sp1');
$db->up('accounts', { balance => 90 }, { id => 1 });
}, { max_retries => 3, isolation => 'SERIALIZABLE' });
# --- Caching ---
my $db = DBIx::Fast->new( SQLite => 'app.db', cache => { default_ttl => 60 } );
my $p = $db->cached(tag => 'products')->hash('SELECT * FROM products WHERE id=?', $id);
$db->insert('products', { sku => 'Y' }); # auto-invalidates 'products' reads
# --- Profiling ---
$db->tracker;
$db->all('SELECT * FROM users');
my $stats = $db->tracker->get_stats; # total/avg time, slow queries, by type
How it compares
DBIx::Fast is not trying to replace ORMs or framework layers — it fills a different niche: thin, multi-driver, batteries-included SQL.
| | DBIx::Fast | DBIx::Class | DBIx::Simple | Mojo::Pg / mysql | DBIx::Connector | |---|:---:|:---:|:---:|:---:|:---:| | Style | thin SQL layer | full ORM | thin query helper | framework DB layer | connection mgr only | | Drivers in one module | SQLite / MariaDB / MySQL / Pg | many (ORM) | any (agnostic) | one per module | any (DBI) | | Dialect-aware upsert / last_id / isolation | ✅ | ✅ | ➖ | ➖ | ➖ | | Fork- & ping-safe reconnect | ✅ built-in | ✅ | ➖ | ✅ (pool) | ✅ | | Deadlock / lock-wait auto-retry | ✅ | manual/plugin | ➖ | ➖ | ➖ | | Savepoints + isolation levels | ✅ | ✅ | ➖ | ➖ | ➖ | | Result cache (TTL + tags) | ✅ built-in | plugin | ➖ | ➖ | ➖ | | Query profiler + EXPLAIN/index tools | ✅ built-in | debug hooks | ➖ | ➖ | ➖ | | Identifier validate+quote / PII redaction | ✅ | ✅ | ➖ | ➖ | n/a | | ORM: relations / row objects / migrations | ❌ by design | ✅ | ❌ | migrations | ❌ | | Object system | Object::Pad | Moo-style | — | Mojo::Base | — |
✅ built-in · ➖ not provided / bring your own · ❌ out of scope
Use DBIx::Fast when you want to write SQL directly across more than one database and not hand-roll connection handling, transactions, caching and safety every time. Reach for an ORM (DBIx::Class, Rose::DB::Object, Teng) when you want to map an object graph with relationships and migrations.
Driver support
| Driver | DBD | Notes | |---|---|---| | SQLite | DBD::SQLite | always tested | | MariaDB | DBD::MariaDB | tested against a live server in CI | | MySQL | DBD::mysql | version-gated upsert (row-alias on 8.0.19+) | | PostgreSQL | DBD::Pg | tested against a live server in CI |
Install
cpanm DBIx::Fast
Core dependencies: DBI, SQL::Abstract, Object::Pad. Optional features pull in
their own deps only when used: LRU::Cache (default cache backend),
Cpanel::JSON::XS and Term::ANSIColor (profiler output).
Testing
The SQLite suite runs everywhere. The driver-specific tests run when you point them at a live server:
DBIX_FAST_TEST_MARIADB='mariadb://root:pass@127.0.0.1:3307/dbix_test' \
DBIX_FAST_TEST_PG='postgres://postgres:pass@127.0.0.1:5433/dbix_test' \
prove -lr t/
For local development, ./dev/test.sh (repo only, not shipped in the dist)
spins up throwaway MariaDB, PostgreSQL and MySQL docker containers and runs
the full suite - all four drivers plus the POD gate. See
dev/README.md for the complete local-setup notes.
./dev/test.sh # start containers (create if missing) + run tests
./dev/test.sh up # start containers only
./dev/test.sh stop # stop them
Documentation
Full reference in the POD: perldoc DBIx::Fast. Subsystems:
Connector,
Transaction,
Result,
Schema,
Cache /
Cached,
Profiler.
Security
DBIx::Fast uses parameterized queries throughout and validates+quotes every identifier. To report a vulnerability, see SECURITY.md (please do not open a public issue).
License
Free software under the Artistic License 2.0. Author: SeHarrys.