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, and the bind values recorded with each error, for PII / PCI (default: 0)
on_error - Coderef called with every error entry as it is recorded (see "errors"), e.g. to forward it to a logger or error tracker. Exceptions thrown by the callback are caught and warned, never propagated into DBI.
strict_reads - With RaiseError => 0, make the read methods (all, hash, val, flat, array, query, count, exists, kv, all_by, execute) throw read failed on a failed prepare or execute, like the write methods already do, instead of returning an empty result (default: 0). Without it a failed read returns [] / undef and only records the error.
timestamps - Automatic created/updated columns for the CRUD writers:
timestamps => {
    default => { created => 'created_at', updated => 'updated_at' },
    tables  => {
        orders => { created => 'date_purchased', updated => 'last_modified' },
        logs   => {},                       # no timestamps for this table
    },
    source  => 'host',    # 'host' (now()), 'utc' (now_utc()) or 'server'
}

insert, insert_ignore, find_or_create and insert_many fill both columns, update/up the updated one, upsert/upsert_many both on insert and only the updated one on conflict (created is never overwritten). A value the row already sets wins, and the caller's hash is not modified. A tables entry replaces default for that table. source => 'server' uses the database's CURRENT_TIMESTAMP instead of the host's clock.

json_columns - Columns stored as JSON text:
json_columns => { '*' => ['meta'], tareas => ['tarea', 'result'] }

On writes (every CRUD method), a hashref/arrayref value of a listed column ('*' = in any table) is encoded with Cpanel::JSON::XS (JSON::PP as a fallback), canonically, before validation. On reads (all, hash, execute), columns with those names are decoded when the value looks like a JSON object or array; other text and malformed JSON are left untouched. Non-ASCII round-trips on every driver, whatever its UTF-8 flags. val, flat and query return the raw text.

on_slow_query - { ms => 200, cb => sub ($q) {...} }: call cb for every statement that took ms milliseconds or more, with { sql => ..., binds => [...], ms => 312.4, caller => 'lib/App.pm:42' } (caller is the application line that ran it). Optional keys: explain (attach the query plan) and redact (hide the binds; defaults to errors_redact). A hook that dies is warned about; the query result is unaffected. See "MONITORING".
metrics - 1 or a label: count statements, durations, slow statements and errors for "metrics_text" (Prometheus). The label defaults to the registry name ("connect") or default. See "MONITORING".
sql_stats - 1 or a number: count executions, total and maximum time per statement for "sql_stats", keeping that many distinct statements (1 = 500). See "MONITORING".
on_repeated_query - { n => 10, cb => sub ($r) {...} }: call cb when one statement runs n times within a "scope_start" .. "scope_end" scope (an N+1 loop). See "MONITORING".
after_write - Coderef called after every completed CRUD write (insert, insert_ignore, update/up, delete, upsert, insert_many, upsert_many) with a hashref describing it:
after_write => sub ($w) {
    # { op => 'update', table => 'orders', data => {...}, where => {...},
    #   rows => 1 }   (inserts also carry last_id; upserts conflict_keys)
    $audit->record($w);
}

data is what was written (after timestamps and json_columns). Raw SQL (exec) is not reported - DBIx::Fast does not know its table. The write is already done when the hook runs: a hook that dies is reported with warn and never fails or undoes the write.

on_connect_sql - Arrayref of trusted SQL statements run on every (re)connect, e.g. [ "SET NAMES utf8mb4", "SET time_zone = '+00:00'" ]. Unlike a one-off exec after construction, the session settings survive a reconnect.
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

CONNECTION REGISTRY

# once, at startup
DBIx::Fast->configure(
    app  => { dsn => $conf->{dsn},  user => $conf->{user}, password => $conf->{pass} },
    logs => { dsn => 'mariadb://...', RaiseError => 0, strict_reads => 1 },
);

# anywhere: models, jobs, scripts
my $db = DBIx::Fast->connect('app');

configure

DBIx::Fast->configure(name => { constructor args }, ...);

Registers named connections (nothing connects yet). Configuring a name again replaces its arguments and drops the instance built from the old ones.

connect

my $db = DBIx::Fast->connect('app');

The process-wide instance for a configured name: built on first use and then returned to every caller, so a process holds one connection per name instead of one per object that needs the database. In a forked child (preforking servers, job workers) the first connect builds the child's own instance; the inherited one is neutralized (see "FORK SAFETY" in DBIx::Fast::Connector). An unknown name throws a DBIx::Fast::X::Usage.

configured

my @names = DBIx::Fast->configured;

forget

DBIx::Fast->forget('app');

Drops a name's instance and configuration (an instance still referenced elsewhere keeps working).

configure_file

# each connection's arguments at a path of the JSON file
DBIx::Fast->configure_file("/home/pws/conf/db.$stage.json", pws => 'db');
DBIx::Fast->configure_file('/opt/itti/conf/iTTi.json', itti => 'db.itti');

# or the whole file is name => { constructor args }
DBIx::Fast->configure_file('/etc/myapp/databases.json');

"configure" from a JSON file (UTF-8), read once at startup instead of in every place that needs a connection. A dotted path walks nested objects and must lead to an object. Errors are DBIx::Fast::X::Usage; an invalid file is reported without the parser's message, which would quote the file's content (credentials).

disconnect_all

DBIx::Fast->disconnect_all;

Disconnects and drops every instance this process built; the configuration stays, so the next connect reconnects. For orderly shutdown and tests.

MONITORING

my $db = DBIx::Fast->new(
    dsn           => $dsn,
    metrics       => 1,
    on_slow_query => { ms => 250, cb => sub ($q) { $log->warn("slow $q->{ms} ms: $q->{sql}") } },
);

# Mojolicious: one /metrics route for every registry connection
$r->get('/metrics' => sub ($c) {
    $c->render(text => DBIx::Fast->metrics_text, format => 'txt');
});

Both options are off by default and then cost nothing; switched on they add about 3 microseconds per statement (timing it, and the cached-statement fast path of val/hash/all is skipped): some 5% of a query over the network, more on an in-memory SQLite one. Statements are timed in every path: reads, writes, exec, stream (the execute; rows are read later). A statement that fails counts as an error, not as a query; a cached hit never reaches the database and counts as a cache hit.

Slow statements

on_slow_query => {
    ms      => 100,
    explain => 1,                                  # development
    redact  => sub ($sql, @binds) { map { /\@/ ? '<email>' : $_ } @binds },
    cb      => sub ($q) { ... },
}

cb receives one hashref per slow statement:

sql, binds, ms - the statement, its bind values and its duration in milliseconds.
caller - "file:line" of the first frame outside DBIx::Fast: the application code that ran the statement.
explain - with explain => 1, the plan of a slow SELECT (or WITH) as rows of hashrefs, the driver's own columns: EXPLAIN on MariaDB/MySQL (type, key, rows...) and Pg (QUERY PLAN), EXPLAIN QUERY PLAN on SQLite (detail). The plan costs one more statement on the same connection (not timed, not counted, its errors ignored), so it is meant for development. undef when it fails or could disturb the connection: another result set still being read (stream, query), a Pg transaction, or Pg with redact on (Pg plans print the bound values).

redact hides the binds from cb: 1 replaces each one with <redacted>, a coderef receives ($sql, @binds) and returns the list to report (if it dies, a single <redacted> is reported). Without the key it follows errors_redact.

metrics

my $m = $db->metrics;
# { queries => 1234, seconds => 0.84, slow => 2,
#   errors  => { Duplicate => 3, Deadlock => 1 },
#   buckets => { 0.001 => 900, 0.005 => 1200, ... },   # cumulative, seconds
#   cache_hits => 50, cache_misses => 9 }               # with a cache

A snapshot of the counters, or undef when metrics is off. Error keys are the DBIx::Fast::X class without its prefix.

reset_metrics

Zeroes the counters.

metrics_text

print $db->metrics_text;           # this connection
print DBIx::Fast->metrics_text;    # every registry connection of this process

The counters in the Prometheus text format: dbix_fast_queries_total, dbix_fast_query_duration_seconds (histogram), dbix_fast_slow_queries_total, dbix_fast_errors_total{class=...} and, with a cache, dbix_fast_cache_hits_total / dbix_fast_cache_misses_total. Every sample carries db (the label) and pid: in a preforking server each worker counts its own statements and a scrape reaches one worker, so the pid label keeps each worker's counters monotonic - aggregate with sum by (db) (rate(...)). Empty when no connection has metrics on.

A scrape still sees only the worker that answered it: under hypnotoad or another preforking server, successive scrapes land on different workers, and a worker that never answers is never seen. To export them all, let each worker write its own text and serve the lot from any of them (the same applies to "sql_stats", also per process):

# every worker, every 15 s (Mojolicious)
Mojo::IOLoop->recurring(15 => sub {
    Mojo::File->new("/run/app-metrics/$$.prom")->spew(DBIx::Fast->metrics_text);
});

# the /metrics route: every live worker's file
$r->get('/metrics' => sub ($c) {
    my @files = grep { kill 0, $_->basename('.prom') }
                Mojo::File->new('/run/app-metrics')->list->each;
    $c->render(text => DBIx::Fast->metrics_merge(map { $_->slurp } @files), format => 'txt');
});

metrics_merge

my $text = DBIx::Fast->metrics_merge(@worker_texts);

Several "metrics_text" outputs as one exposition. Concatenating them is not valid Prometheus text (a family's samples must come in one group, under one # HELP/# TYPE); this groups them per family. The pid label keeps the workers' samples apart.

metrics_on

True when the metrics option is set.

metrics_label

The db label of the samples.

sql_stats

my $db = DBIx::Fast->new(dsn => $dsn, sql_stats => 1);
...
for my $s (@{ $db->sql_stats(top => 10) }) {
    printf "%8.1f ms %6d x  %s\n", $s->{seconds} * 1000, $s->{count}, $s->{sql};
}
# [ { sql => 'SELECT * FROM t WHERE id IN (?, ...)', count => 812,
#     seconds => 1.94, max => 0.21 }, ... ]

Where the database time goes, by statement: the sql_stats option keeps, per distinct SQL, its executions, total seconds and slowest run. order is seconds (default), count or max, descending; top limits the list. undef when the option is off.

The key is the SQL text with placeholder lists collapsed: IN (?) and IN (?, ?, ?) are both IN (?, ...), and multi-row VALUES of any length one statement. SQL that interpolates its values (instead of binding them) is a new statement for every value: once the limit of distinct statements is reached (sql_stats => 1 = 500), new ones are added up under (other) - memory stays bounded, and a large (other) points at that interpolation. The statements are not exported by "metrics_text": SQL text as a Prometheus label would be unbounded cardinality.

reset_sql_stats

Empties the sql_stats counters.

scope_start

scope_end

my $db = DBIx::Fast->new(
    dsn               => $dsn,
    on_repeated_query => { n => 20, cb => sub ($r) {
        # { sql => 'SELECT * FROM stock WHERE product_id = ?', count => 20,
        #   seconds => 0.012, caller => 'lib/App/Controller/Cat.pm:88' }
        $log->warn("N+1? $r->{sql} x$r->{count} at $r->{caller}");
    } },
);

# Mojolicious: one scope per request
$app->hook(before_dispatch => sub ($c) { $c->app->db->scope_start });
$app->hook(after_dispatch  => sub ($c) {
    my $s = $c->app->db->scope_end or return;
    # { queries => 307, seconds => 0.195, statements => 4,
    #   repeated => [ { sql => ..., count => 300, seconds => 0.17 }, ... ] }
});

A scope counts every statement run between scope_start and scope_end (the same keys as "sql_stats"; it works without that option). scope_end closes it and returns the queries, seconds and distinct statements of the scope, with repeated listing those run more than once, most repeated first. undef when no scope was open; a new scope_start discards an open one.

on_repeated_query fires inside a scope, once per statement, when it reaches n runs. caller is the application line of that n-th run: usually the loop that should become one IN (?) query or a join. A hook that dies is warned about.

While a scope is open, or with sql_stats on, every statement is timed and counted: the cost of metrics plus under a microsecond. The counters belong to the object: in a non-blocking server that interleaves requests on the same connection object, scopes overlap and count each other's statements.

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.

rows

Rows affected by the last write, as a plain number (0, never '0E0'), or undef when the driver does not know. See "rows" in DBIx::Fast::SQL.

errors

All retained errors as an arrayref (read-only, capped by max_errors). Each entry is a hashref:

{ id => $driver_err, error => $message, time => $epoch, seq => $n,
  sql => $statement, binds => [ @bind_values ] }

sql/binds are the statement in flight when the error was recorded (binds are '<redacted>' under errors_redact).

error_mark / errors_since

my $mark = $db->error_mark;
... handle a request / run a job ...
my $new = $db->errors_since($mark);   # arrayref of entries

Scope errors to a unit of work. Sequence-based, so it stays correct after the max_errors cap has dropped older entries.

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 a DBIx::Fast::X::Usage exception, 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 (as Last error: [id] message, without a timestamp so identical failures produce identical text) only when it was recorded within the last few seconds (likely causal context).

Typed exceptions

Everything DBIx::Fast throws is a DBIx::Fast::X object that stringifies to the same text a plain string exception used to carry, so $@ =~ /.../ and logging keep working. Database errors are classified by driver error code / SQLSTATE:

eval { $db->insert('users', { email => $email }) };
if    ($@ isa DBIx::Fast::X::Duplicate) { ... $@->constraint ... }
elsif ($@ isa DBIx::Fast::X::Deadlock)  { ... retry ... }
elsif ($@ isa DBIx::Fast::X::DB)        { log($@->code, $@->sql) }
elsif ($@) { die $@ }

Classes: Usage (validation), and DB with Duplicate, ForeignKey, NotNull, Deadlock, LockTimeout, Syntax, Connection. See DBIx::Fast::X for the fields (code, state, sql, binds, constraint, ...).

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[a-zA-Z_][a-zA-Z0-9_.]*\z/, 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. Everything a JSON/form decoder can produce is locked down: in insert/update data a hashref or arrayref value is rejected (SQL::Abstract would read ['(SELECT ...)'] or { -literal => ... } as raw SQL), and in a WHERE spec only the bind-only dash-ops are accepted (-and, -or, -in, -not_in, -between, -not_between, -like, -not_like, -is, -is_not, -value); -literal, -func, -ident and the rest are refused.

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[a-zA-Z_][a-zA-Z0-9_.]*\z/. 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; typed exceptions: DBIx::Fast::X; Redis cache backend: DBIx::Fast::Cache::Redis; unit tests without a server: DBIx::Fast::Test::Mock.

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