NAME
Punk::Model::DBI - the default DBI backend for Punk models
DESCRIPTION
The shipped Punk::Model backend: plain DBI, no ORM. Connections are pooled by dsn and shared across every model that uses them, so a hundred models on one database open one handle per worker, not a hundred; a $$ check reconnects after a fork. RaiseError and AutoCommit are on, and generated SQL is prepared through prepare_cached so each distinct statement is compiled once.
It is selected by default; database backend => 'Class' swaps it for any class honouring the same six methods. Every call blocks the worker for the whole database round trip - Punk::Model::DBIx::Loop is the non-blocking alternative, at the cost of handlers written against futures.
CONFIGURATION
From the database keyword:
database
dsn => 'dbi:SQLite:dbname=myapp.db',
user => $user, # optional
password => $pass, # optional
attr => { ... }; # optional, merged into the connect attrs
CONSTRUCTOR
new
Punk::Model::DBI->new(database => \%conn, table => $t,
primary => $pk, columns => \@names);
Built by "_instantiate" in Punk::Model from the database options and the model's table, primary key and columns. Not called directly.
dbh
The live per-worker DBI handle for this backend's dsn, connected on first use and shared with every other backend on the same database.
THE CONTRACT
get(%key)
SELECT * ... WHERE key = ? ... - the row hashref, or undef.
search(\%filter, \%opts)
The filter with its operators ("The filter" in Punk::Model), ORDER BY the order_by columns with the primary key as the tie-breaker ("Ordering" in Punk::Model), LIMIT one past limit (default 20). Returns
{ rows => [ \%row, ... ], has_more_data => 0|1, next => $token|undef }
next is a url-safe, opaque encoding of the last row's values under the ordering. Passed back as after => $token it continues from that row - under a single column a WHERE pk ?>, under several the expanded comparison (a < ?) OR (a = ? AND b ?) OR ...> with each column in its own direction - so pagination is seek-based, not offset-based, and a mixed ordering pages correctly. Identifiers are quoted through the connection and values bound; keys are sorted so one filter shape is one prepared statement.
count(\%filter)
SELECT COUNT(*) ... WHERE ... for the same filter; the number.
all()
search({}, {}).
transactions
$c->txn on this backend is begin_work, the block, commit - or rollback and a rethrow when the block dies or the commit fails. There is one connection per database per worker, so every model on that database is inside the transaction for the length of the block, whether it came through $tx->model or $c->model, and so is a raw statement on $tx->handle, which is the $dbh. A second txn inside the block croaks: nested transactions are not supported, and a silent join would stop a rollback covering what the outer block thought it covered.
create(\%data)
Inserts the known columns and returns the stored row (via RETURNING where the driver supports it - SQLite 3.35+ or PostgreSQL, detected once per connection - otherwise re-fetched by primary key).
update(\%key_and_changes)
Updates the row named by the primary key with the remaining columns; returns the stored row.
delete(%key)
Deletes and returns the affected row count.
OBSERVED STATEMENTS
When something has registered a query observer through pk_abi, the connection is built with Punk::DBI as its RootClass and every statement on that handle is reported - the six methods above, and equally anything run through $model->backend->dbh directly.
That second half is the point. The six methods are a layer above the handle, and an application reaches past them for whatever the filter language cannot express: an OR, a UNION, a FOR UPDATE, an upsert. Those statements are usually the interesting ones, and while the observer lived on the layer they were invisible with nothing to say so.
The observer is handed the statement text and the number of bind values, never the values themselves: they are the literal data, and the SQL carries placeholders exactly where they would have been.
What it costs
A sub call per statement, for an application that asked to see its statements. With no observer registered the handle is a plain DBI::db and there is no wrapper at all.
An application that passes its own RootClass in attr keeps it, and does not get the statement observer - the subclass it asked for wins over the one this would have installed.
SEE ALSO
Punk::Model, Punk, Punk::DBI, DBI.
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)