DBIx::Fast

DBI, fast & easy — one small API over SQLite, MariaDB, MySQL and PostgreSQL.

Perl Object::Pad License

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:

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.