package Punk::Model::DBI;

use 5.010;
use strict;
use warnings;
use Punk ();
use File::Raw::JSON ();

our $VERSION = '0.34';

1;

__END__

=head1 NAME

Punk::Model::DBI - the default DBI backend for Punk models

=head1 DESCRIPTION

The shipped L<Punk::Model> backend: plain L<DBI>, no ORM. Connections are
pooled by C<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 C<$$> check reconnects after a fork. C<RaiseError> and C<AutoCommit> are
on, and generated SQL is prepared through C<prepare_cached> so each
distinct statement is compiled once.

It is selected by default; C<< database backend => 'Class' >> swaps it
for any class honouring the same six methods. Every call blocks the worker
for the whole database round trip - L<Punk::Model::DBIx::Loop> is the
non-blocking alternative, at the cost of handlers written against futures.

=head1 CONFIGURATION

From the C<database> keyword:

    database
        dsn      => 'dbi:SQLite:dbname=myapp.db',
        user     => $user,        # optional
        password => $pass,        # optional
        attr     => { ... };      # optional, merged into the connect attrs

=head1 CONSTRUCTOR

=head2 new

    Punk::Model::DBI->new(database => \%conn, table => $t,
                          primary => $pk, columns => \@names);

Built by L<Punk::Model/_instantiate> from the C<database> options and the
model's table, primary key and columns. Not called directly.

=head2 dbh

The live per-worker L<DBI> handle for this backend's C<dsn>, connected on
first use and shared with every other backend on the same database.

=head1 THE CONTRACT

=head2 get(%key)

C<SELECT * ... WHERE key = ? ...> - the row hashref, or undef.

=head2 search(\%filter, \%opts)

The filter with its operators (L<Punk::Model/"The filter">), C<ORDER BY>
the C<order_by> columns with the primary key as the tie-breaker
(L<Punk::Model/Ordering>), C<LIMIT> one past C<limit> (default 20).
Returns

    { rows => [ \%row, ... ], has_more_data => 0|1, next => $token|undef }

C<next> is a url-safe, opaque encoding of the last row's values under the
ordering. Passed back as C<< after => $token >> it continues from that
row - under a single column a C<WHERE pk > ?>, under several the expanded
comparison C<(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.

=head2 count(\%filter)

C<SELECT COUNT(*) ... WHERE ...> for the same filter; the number.

=head2 all()

C<search({}, {})>.

=head2 transactions

C<< $c->txn >> on this backend is C<begin_work>, the block, C<commit> - or
C<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 C<< $tx->model >> or C<< $c->model >>, and so is a raw statement on
C<< $tx->handle >>, which is the C<$dbh>. A second C<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.

=head2 create(\%data)

Inserts the known columns and returns the stored row (via C<RETURNING>
where the driver supports it - SQLite 3.35+ or PostgreSQL, detected once
per connection - otherwise re-fetched by primary key).

=head2 update(\%key_and_changes)

Updates the row named by the primary key with the remaining columns;
returns the stored row.

=head2 delete(%key)

Deletes and returns the affected row count.

=head1 SEE ALSO

L<Punk::Model>, L<Punk>, L<DBI>.

=head1 AUTHOR

LNATION <email@lnation.org>

=head1 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)

=cut
