NAME

DBIx::Fast - DBI fast & easy

SYNOPSIS

use DBIx::Fast;

# Connect via a DBI DSN
my $db = DBIx::Fast->new(
    dsn      => 'dbi:MariaDB:dbname=mydb;host=localhost',
    user     => 'root',
    password => 'secret',
);

# Connect via a URI (postgres|postgresql|mariadb|mysql|sqlite)
my $db = DBIx::Fast->new( dsn => 'mariadb://root:secret@localhost:3306/mydb' );

# SQLite shortcut
my $db = DBIx::Fast->new( SQLite => '/path/to/db.sqlite' );

# Dependency injection
my $db = DBIx::Fast->new( db => $connector, driver => 'SQLite' );

# Queries
$db->all('SELECT * FROM users WHERE active = ?', 1);
my $rows = $db->results;

my $user = $db->hash('SELECT * FROM users WHERE id = ?', $id);
my $name = $db->val('SELECT name FROM users WHERE id = ?', $id);
my @ids  = $db->flat('SELECT id FROM users');
my $total = $db->count('users', { active => 1 });

# CRUD
$db->insert('users', { name => 'Alice', status => 1 }, time => 'created_at');
$db->up('users', { name => 'Bob' }, { id => 1 });
$db->up('users', { name => 'Bob' }, { id => 1 }, 'updated_at');
$db->delete('users', { id => 1 });

# Transactions
$db->txn(sub {
    $db->insert('orders', { total => 100 });
    $db->insert('order_items', { order_id => $db->last_id, product => 'Widget' });
});

# Named parameters
$db->execute('SELECT * FROM users WHERE name = :name', { name => 'Alice' });

# Row iterator (does not load all rows into memory)
my $rs = $db->query('SELECT * FROM logs ORDER BY id DESC LIMIT 1000');
while (my $row = $rs->hash) {
    process($row);
}

# Transactions with automatic deadlock retry, savepoints and 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' });

# Cached reads (TTL + tag invalidation; CRUD writes auto-invalidate the table)
my $cdb = DBIx::Fast->new( SQLite => 'app.db', cache => { default_ttl => 60 } );
my $row = $cdb->cached(ttl => 60, tag => 'users')
              ->hash('SELECT * FROM users WHERE id = ?', $id);

# Query profiling
$db->tracker;                                 # enable tracking
$db->all('SELECT * FROM users');
my $stats = $db->tracker->get_stats;          # timings, slow queries, by type

DESCRIPTION

DBIx::Fast is a lightweight database access layer built on DBI and SQL::Abstract. It 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, behind one small API that works the same across SQLite, MariaDB, MySQL and PostgreSQL.

Built with Object::Pad; requires Perl v5.38 or later.

FEATURES

  • Multi-driver, dialect-aware - one API over SQLite, MariaDB, MySQL and PostgreSQL. Driver differences (identifier quoting, upsert syntax, last-insert id, isolation-level SQL, schema introspection) are handled internally.

  • Resilient connections - fork-safe handles with lazy connect, throttled server-side ping to detect dropped connections, automatic reconnect, and a guard that fails loudly (rather than silently committing) if a connection is lost mid-transaction. See DBIx::Fast::Connector.

  • Transactions that retry - txn runs a block in a transaction with automatic deadlock / lock-wait retry (detected by driver error code), nested transactions, named savepoints and per-transaction isolation levels. See DBIx::Fast::Transaction.

  • Result caching - optional in-process (or CHI-backed) cache of query results with TTL and tag-based invalidation; CRUD writes auto-invalidate the affected table. See DBIx::Fast::Cache and DBIx::Fast::Cached.

  • Query profiling - a built-in tracker (timings, slow-query detection, per-type statistics) plus driver-native diagnostics such as EXPLAIN and index / connection analysis. See DBIx::Fast::Profiler, DBIx::Fast::Profile::MariaDB, DBIx::Fast::Profile::mysql and DBIx::Fast::Profile::Pg.

  • Security-conscious - values always go through placeholders; identifiers are validated and quoted; count operators, isolation levels and savepoint names are whitelisted; error messages can optionally redact quoted literals (PII / PCI). See "SECURITY".

  • Flexible result shapes - all / hash / val / flat / array, a memory-efficient row iterator (query), and both positional (?) and named (:name) parameters.

When to use it

DBIx::Fast is a good fit when you want to write SQL directly but not re-implement connection handling, transactions, caching and safety every time. It is not an ORM: there are no result classes, relationships or migrations. If you need to map an object graph, reach for a full ORM instead (see "SEE ALSO").

Supported drivers

SQLite (DBD::SQLite), MariaDB (DBD::MariaDB), MySQL (DBD::mysql) and PostgreSQL (DBD::Pg). The test suite runs against SQLite always, and against live MariaDB and PostgreSQL when DBIX_FAST_TEST_MARIADB / DBIX_FAST_TEST_PG are set.

CONSTRUCTOR

new

my $db = DBIx::Fast->new(%args);

Accepted parameters:

dsn - a native DBI DSN (dbi:DRIVER:..., case-insensitive) or a URI. URI userinfo is optional, so a credential-less mariadb://localhost/db (socket / peer authentication) is accepted as well as mariadb://user:pass@host:port/db.
SQLite - Path to SQLite database file (shortcut). For an in-memory database use SQLite => ':memory:' or the DSN dbi:SQLite::memory:.
db - Database name string or pre-built DBIx::Fast::Connector object
driver - Database driver: SQLite, Pg, MariaDB, mysql
user, password, host, port - Connection credentials
RaiseError, PrintError, AutoCommit - DBI attributes (defaults: 1, 0, 1)
mysql_enable_utf8 - Enable UTF-8 on MySQL/MariaDB (mapped to the driver-appropriate attribute: mariadb_enable_utf8mb4 / mysql_enable_utf8mb4). Default: 0
tn - Enable table-name validation against the schema (default: 0)
abstract - Enable SQL::Abstract for CRUD (default: 1)
cache - Enable result caching: a hashref of DBIx::Fast::Cache options (e.g. { default_ttl => 60, max_size => 1000 }). Omit to disable. See "cached".
max_errors - Cap on the retained error history, FIFO (default: 100)
errors_redact - Redact quoted literals in captured error messages, for PII / PCI (default: 0)
profile_output - Output routing for the profiler subsystem, applied to both "tracker" and "profiler": 'text' (default, formatted reports), 'json' (one JSON line per event) or a coderef called as $cb->($event, $data, $meta). See DBIx::Fast::Output.
profile_output_fh - Filehandle the text/json profiler output is written to (default: the currently selected handle, normally STDOUT). For a log file: open my $fh, '>>', $path; chmod 0600, $path; - profiler events carry cleartext SQL and bind values.
trace, profile - DBI tracing / profiling options

ACCESSORS

db

DBIx::Fast::Connector instance (read/write).

dbd

Database driver name: SQLite, Pg, MariaDB, or mysql (read-only).

dsn

Processed DSN string (read-only).

Q

SQL::Abstract instance (read-only).

sql

Current SQL statement (read/write; part of the q/make_sen workflow).

last_sql

Last executed SQL statement (read-only).

p

Current bind parameters arrayref (read/write).

results

Last query result (read-only). Reflects only the most recent query operation.

last_id

Last insert id (read-only). Reflects only the most recent insert; the authoritative value is the return value of insert / insert_many.

errors

All errors as arrayref (read-only).

last_error

Last error message string (read-only).

args

Processed constructor configuration hashref (read-only).

QUERY AND CRUD METHODS

All query methods (all, flat, hash, val, array, query, count, exec, execute) and CRUD methods (insert, update, up, delete, upsert, insert_many, upsert_many), together with q, make_sen and execute_prepare, are composed into this class from the DBIx::Fast::SQL role - the API is identical to previous releases, only the reference documentation lives on that page.

RAW SQL

You are never limited to the CRUD helpers: write whatever SQL you like and pick the method by what you want back. Values always go in as ? placeholders (bound, injection-safe) - never interpolate data into the string yourself.

# SELECT - pick the return shape:
my $rows  = $db->all ('SELECT * FROM users WHERE age > ?', 18);   # arrayref of hashrefs
my $row   = $db->hash('SELECT * FROM users WHERE id = ?', 42);    # one hashref
my $count = $db->val ('SELECT COUNT(*) FROM users');              # one scalar
my @names = $db->flat('SELECT name FROM users');                  # flat list of a column

# Big result set - stream it row by row instead of slurping:
my $rs = $db->query('SELECT id, name FROM users ORDER BY id');
while (my $u = $rs->hash) { say "$u->{id}: $u->{name}" }

# Anything else (DDL / INSERT / UPDATE / DELETE / vendor SQL):
$db->exec('UPDATE users SET seen = ? WHERE id = ?', $now, 42);
$db->exec('CREATE INDEX idx_users_email ON users (email)');

# Named placeholders (:name) when positional ? is awkward:
$db->execute('SELECT * FROM users WHERE name = :n AND age > :a',
             { n => 'Alice', a => 18 });

The last statement, its bind values and result are always available via $db->sql, $db->p and $db->results.

Placeholders are for values, not identifiers. Table and column names cannot be bound; if one must be dynamic, validate it against your own allow-list or quote it with $db->_quote_id($name) - never build it from raw user input. See "SECURITY".

Full reference for each method: DBIx::Fast::SQL.

SUBSYSTEMS

schema

my $schema = $db->schema;

Returns the DBIx::Fast::Schema instance (lazy-loaded) for table introspection.

transaction

my $tx = $db->transaction;

Returns the DBIx::Fast::Transaction instance (lazy-loaded).

txn

$db->txn(sub { ... });
$db->txn(sub { ... }, { max_retries => 5 });

Shortcut for $db->transaction->do(...). Executes a code block inside a transaction with automatic commit/rollback and deadlock retry.

profiler

my $profiler = $db->profiler;

Returns the driver-specific profile instance (DBIx::Fast::Profile::MariaDB, DBIx::Fast::Profile::mysql, DBIx::Fast::Profile::Pg, DBIx::Fast::Profile::SQLite, etc.) for native database diagnostics. Lazy-loaded on first access.

tracker

my $tracker = $db->tracker;

Returns the DBIx::Fast::Profiler instance for query tracking. Once activated, all queries executed through all, hash, val, flat, array, exec, insert, update, up, and delete are recorded with timing information.

# Activate tracking
$db->tracker;

# Run some queries
$db->all('SELECT * FROM users');
$db->insert('logs', { action => 'login' });
$db->up('users', { last_login => $db->now }, { id => 1 });

# Get statistics
my $stats = $db->tracker->get_stats;
printf "Queries: %d, Total: %.4fs, Avg: %.4fs\n",
    $stats->{total_queries}, $stats->{total_time}, $stats->{avg_time};

# Slow queries
my $slow = $db->tracker->get_slow_queries(5);
for my $q (@$slow) {
    printf "%.4fs - %s\n", $q->{duration}, $q->{sql};
}

# Stats by type (SELECT, INSERT, UPDATE, DELETE)
my $by_type = $db->tracker->get_detailed_stats;

# Print formatted report
$db->tracker->print_stats;

# Or route every report as structured data instead of text:
$db->tracker->output('json');          # one JSON line per event
$db->tracker->output(sub ($event, $data, $meta) { $log->info(...) });

# Clear recorded queries
$db->tracker->clear;

cached

# Enable caching at construction:
my $db = DBIx::Fast->new( SQLite => 'app.db', cache => { default_ttl => 60 } );

# Read through the cache; tag it with the table so writes can invalidate it:
my $row  = $db->cached(ttl => 60, tag => 'products')
              ->hash('SELECT * FROM products WHERE id = ?', $id);
my $rows = $db->cached(tag => 'products')->all('SELECT * FROM products');

Returns a DBIx::Fast::Cached proxy that wraps the query methods (hash/val/all/array/flat) with a cache lookup keyed on (method, SQL, bind values). On a miss the query runs and the result is stored; on a hit the stored value is returned (including cached "no row" results).

Options: ttl (seconds; falls back to the cache's default_ttl) and tag/tags (labels for invalidation). Writes through insert/update/ up/delete/upsert automatically invalidate cached reads tagged with the affected table name. Reads that are untagged, span multiple tables, or are mutated via raw exec/query/execute are not auto-invalidated - tag them and/or call "invalidate" yourself.

If the instance was constructed without cache, the proxy transparently passes through with no caching and no overhead.

cache

Returns the underlying DBIx::Fast::Cache instance (or undef if caching is disabled) for direct get/set/invalidate/stats access.

invalidate

$db->invalidate('products');            # after a raw-SQL write
$db->invalidate(@tables);

Drops every cached entry tagged with the given tag(s). Use it after mutations that bypass the CRUD auto-invalidation (raw exec/query/execute, or reads spanning several tables). No-op when caching is disabled.

load_extension

my $ext = $db->load_extension('Schema');

Dynamically loads and caches a DBIx::Fast::* extension module.

UTILITY METHODS

now

Returns the current local timestamp as YYYY-MM-DD HH:MM:SS.

now_utc

Like "now" but in UTC (gmtime). Use it when the database session timezone differs from the host's, so stored timestamps stay comparable.

set_error

$db->set_error($code, $message);

Appends an error to the errors array and updates last_error.

TableName

my $table = $db->TableName('users');

Validates a table name. When tn => 1 is set, checks that the table exists in the schema cache.

Exception

$db->Exception("Something went wrong");

Throws an exception via croak, unconditionally - validation and usage errors are never silenced by RaiseError/PrintError settings (a swallowed validation error once let rejected identifiers flow on and become an injection vector). The message honors errors_redact, and the current last_error is appended only when it was recorded within the last few seconds (likely causal context).

SECURITY

DBIx::Fast uses multiple layers of defense against SQL injection:

  • Bind parameters - All query methods (all, hash, val, flat, array, exec, query) pass values through DBI placeholders (?). Values are never interpolated into SQL strings.

  • SQL::Abstract - CRUD operations (insert, update, delete) delegate SQL generation to SQL::Abstract, which produces parameterized queries.

  • Identifier validation - Table and column names are validated by _safe_id against the pattern /^[a-zA-Z_][a-zA-Z0-9_.]*$/, rejecting any special characters, spaces, or SQL syntax.

  • Identifier quoting - After validation, identifiers are quoted via DBI's quote_identifier() (_quote_id) as defense-in-depth.

  • Operator whitelist - The count WHERE builder only accepts operators from a fixed whitelist: =, !=, <>, <, <=, >, >=, LIKE, NOT, BETWEEN, IS. The SQL::Abstract-backed CRUD methods (insert, update, delete, upsert) enforce their own whitelist on every nested operator key of a WHERE/SET spec (_safe_op_tree), because SQL::Abstract interpolates those keys verbatim into the statement. The whitelist is enforced for every WHERE shape SQL::Abstract accepts - hashref, arrayref (OR-of-conditions) and nested combinations; a scalarref (literal SQL) WHERE is rejected outright from these methods.

  • Named parameter substitution - make_sen converts :name placeholders to ? with a bind array, never interpolating values.

Caveat - literal SQL values: following SQL::Abstract semantics, a scalar-ref value in a CRUD spec ({ col => \"NOW()" }) is inserted as literal SQL, not bound. Only ever pass references you wrote yourself; data coming from a request must always be a plain scalar.

Caveat - profiler log: the profile option writes a DBI profile log (dbix-fast-PID.log, containing SQL text) to the current working directory with default permissions. Do not enable it when the CWD is a shared or world-writable directory.

Internal security methods

_safe_id

$db->_safe_id($identifier);

Validates that $identifier matches /^[a-zA-Z_][a-zA-Z0-9_.]*$/. Throws an exception on failure. Used for table names, column names, and any identifier that cannot be passed as a bind parameter.

_quote_id

my $quoted = $db->_quote_id($identifier);

Validates via _safe_id then quotes with $dbh->quote_identifier(). Returns driver-appropriate quoted identifier (backticks for MySQL/MariaDB, double quotes for SQLite/Pg).

REPORTING SECURITY ISSUES

If you believe you have found a security vulnerability in DBIx::Fast, please do not file a public GitHub issue.

Report it privately via GitHub's private vulnerability reporting: https://github.com/SeHarrys/dbix-fast/security/advisories/new

See SECURITY.md in the distribution root for the full security policy.

SEE ALSO

Subsystems: DBIx::Fast::Connector, DBIx::Fast::Transaction, DBIx::Fast::Result, DBIx::Fast::Schema, DBIx::Fast::Cache, DBIx::Fast::Cached, DBIx::Fast::Profiler, DBIx::Fast::Profile::MariaDB, DBIx::Fast::Profile::Pg.

Built on: DBI, SQL::Abstract, Object::Pad.

Other database layers on CPAN, depending on what you need: full ORMs (DBIx::Class, Rose::DB::Object, Teng), thinner query helpers (DBIx::Simple), framework-integrated layers (Mojo::Pg, Mojo::mysql), and standalone connection management (DBIx::Connector).

AUTHOR

SeHarrys

LICENSE AND COPYRIGHT

This is free software under the Artistic License 2.0. http://www.perlfoundation.org/artistic_license_2_0