# DBIx::Fast

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

![Perl](https://img.shields.io/badge/perl-5.38%2B-blue)
![Object::Pad](https://img.shields.io/badge/built%20with-Object::Pad-purple)
![License](https://img.shields.io/badge/license-Artistic%202.0-green)

DBIx::Fast sits between a bare [DBI](https://metacpan.org/pod/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.

```perl
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, `upsert` syntax, last-insert id,
  isolation-level SQL and schema introspection all adapt to the driver.
- **Resilient connections.** Fork-safe handles, lazy connect, throttled
  server-side `ping` to 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.** `txn` runs 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](https://metacpan.org/pod/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 `EXPLAIN` and index/connection
  analysis.
- **Security-conscious.** Values always use placeholders; identifiers are
  validated and quoted; `count` operators, isolation levels and savepoint names
  are whitelisted; error messages can optionally redact quoted literals (PII/PCI).
- **Modern & light.** Built with [Object::Pad](https://metacpan.org/pod/Object::Pad)
  (Perl 5.38+). Tiny core; the profiler and cache are lazily loaded only if used.

## Quick start

```perl
# --- 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](https://metacpan.org/pod/DBIx::Class),
[Rose::DB::Object](https://metacpan.org/pod/Rose::DB::Object),
[Teng](https://metacpan.org/pod/Teng)) when you want to map an object graph with
relationships and migrations.

## Driver support

| Driver | DBD | Notes |
|---|---|---|
| SQLite | [DBD::SQLite](https://metacpan.org/pod/DBD::SQLite) | always tested |
| MariaDB | [DBD::MariaDB](https://metacpan.org/pod/DBD::MariaDB) | tested against a live server in CI |
| MySQL | [DBD::mysql](https://metacpan.org/pod/DBD::mysql) | version-gated upsert (row-alias on 8.0.19+) |
| PostgreSQL | [DBD::Pg](https://metacpan.org/pod/DBD::Pg) | tested against a live server in CI |

## Install

```sh
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:

```sh
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`](dev/README.md) for the complete local-setup notes.

```sh
./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](https://metacpan.org/pod/DBIx::Fast::Connector),
[Transaction](https://metacpan.org/pod/DBIx::Fast::Transaction),
[Result](https://metacpan.org/pod/DBIx::Fast::Result),
[Schema](https://metacpan.org/pod/DBIx::Fast::Schema),
[Cache](https://metacpan.org/pod/DBIx::Fast::Cache) /
[Cached](https://metacpan.org/pod/DBIx::Fast::Cached),
[Profiler](https://metacpan.org/pod/DBIx::Fast::Profiler).

## Security

DBIx::Fast uses parameterized queries throughout and validates+quotes every
identifier. To report a vulnerability, see [SECURITY.md](SECURITY.md) (please do
not open a public issue).

## License

Free software under the Artistic License 2.0. Author: SeHarrys.
