Partly because some errors currently appeared to come from within FU::PG itself, which is useless, and partly because it's common to wrap database access methods, while that's exactly the kind of operation where you *really* want to know where the error originated from. (Source: too much time wasted debugging VNDB errors)
425 lines
12 KiB
Perl
425 lines
12 KiB
Perl
package FU::PG 0.1;
|
|
use v5.36;
|
|
use FU::XS;
|
|
|
|
_load_libpq();
|
|
|
|
package FU::PG::conn {
|
|
sub lib_version { FU::PG::lib_version() }
|
|
|
|
sub txn($c) {
|
|
$c->exec('BEGIN');
|
|
$c->_set_cookie(++$FU::PG::txn::COUNTER);
|
|
bless [$c, $FU::PG::txn::COUNTER, undef], 'FU::PG::txn';
|
|
}
|
|
};
|
|
|
|
package FU::PG::txn {
|
|
use Carp 'confess';
|
|
|
|
my $COUNTER = 0;
|
|
|
|
# Arrayref:
|
|
# 0: $conn
|
|
# 1: $cookie, a snapshot of $COUNTER that identifies this transaction, used
|
|
# to match commands against transactions. Set to undef when this
|
|
# transaction is 'done' but the object is still alive.
|
|
# 2: $parent, undef if this is a top-level transaction.
|
|
|
|
sub commit($t) {
|
|
confess "Unable to commit transaction that has already finished" if !$t->[1];
|
|
$t->exec($t->[2] ? "RELEASE SAVEPOINT fupg_$t->[1]" : 'COMMIT');
|
|
$t->[1] = undef;
|
|
}
|
|
|
|
sub rollback($t) {
|
|
confess "Unable to rollback transaction that has already finished" if !$t->[1];
|
|
$t->exec($t->[2] ? "ROLLBACK TO SAVEPOINT fupg_$t->[1]" : 'ROLLBACK');
|
|
$t->[1] = undef;
|
|
}
|
|
|
|
sub txn($t) {
|
|
confess "Unable to create sub-transaction when current transaction has already finished" if !$t->[1];
|
|
$COUNTER++;
|
|
$t->exec("SAVEPOINT fupg_$COUNTER");
|
|
$t->[0]->_set_cookie($COUNTER);
|
|
bless [$t->[0], $COUNTER, $t], 'FU::PG::txn';
|
|
}
|
|
|
|
sub status($t) {
|
|
my $cs = $t->[0]->status;
|
|
return $cs if $cs eq 'bad' || ($t->[1] && $t->[0]->_get_cookie != $t->[1]);
|
|
return $cs eq 'txn_error' ? 'error' : $t->[1] ? 'idle' : 'done';
|
|
}
|
|
|
|
sub DESTROY($t) {
|
|
# Can't really throw an error in DESTROY. If a rollback command fails,
|
|
# we're sufficiently screwed that the only sensible recourse is to
|
|
# disconnect and let any further operations throw an error.
|
|
eval { $t->rollback; 1 } || $t->[0]->disconnect if $t->[1];
|
|
$t->[0]->_set_cookie($t->[2] ? $t->[2][1] : 0);
|
|
}
|
|
}
|
|
|
|
package FU::PG::error {
|
|
use overload '""' => sub($e, @) { $e->{full_message} };
|
|
}
|
|
|
|
1;
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
FU::PG - Another PostgreSQL client module
|
|
|
|
=head1 SYNOPSYS
|
|
|
|
my $conn = FU::PG->connect("dbname=test user=test password=nottest");
|
|
|
|
$conn->exec('CREATE TABLE books (id SERIAL, title text)');
|
|
|
|
$conn->q('INSERT INTO books (title) VALUES ($1)', 'Revelation Space')->exec;
|
|
|
|
for my ($id, $title) ($conn->q('SELECT * FROM books')->flat->@*) {
|
|
print "$id: $title\n";
|
|
}
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
FU::PG is a PostgreSQL client module that (attempts) to set itself apart from
|
|
the existing alternatives by offering the following features:
|
|
|
|
=over
|
|
|
|
=item * Automatic conversion of complex types (like JSON, hstore, records, etc)
|
|
to and from convenient corresponding perl values.
|
|
|
|
=item * Support for custom types.
|
|
|
|
=item * Configurable Perl representation of timestamp values (or, well, really
|
|
for any type).
|
|
|
|
=item * Data is transfered in the binary format (which may or may not be more
|
|
efficient, need benchmarks).
|
|
|
|
=item * Convenient and high-level API.
|
|
|
|
=back
|
|
|
|
=head2 Connection setup
|
|
|
|
=over
|
|
|
|
=item B<< FU::PG->connect($string) >>
|
|
|
|
Connect to the PostgreSQL server and return a new C<FU::PG::conn> object.
|
|
C<$string> can either be in key=value format or a URI, refer to L<the
|
|
PostgreSQL
|
|
documentation|https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING>
|
|
for the full list of supported formats and options. You may also pass an empty
|
|
string and leave the configuration up L<environment
|
|
variables|https://www.postgresql.org/docs/current/libpq-envars.html>.
|
|
|
|
=item B<< $conn->server_version >>
|
|
|
|
Returns the version of the PostgreSQL server as an integer in the format of
|
|
C<$major * 10000 + $minor>. For example, returns 170002 for PostgreSQL 17.2.
|
|
|
|
=item B<< $conn->lib_version >>
|
|
|
|
Returns the libpq version in the same format as the C<server_version> method.
|
|
Also available directly as C<FU::PG::lib_version()>.
|
|
|
|
=item B<< $conn->status >>
|
|
|
|
Returns a string indicating the status of the connection. Note that this method
|
|
does not verify that the connection is still alive, the status is updated after
|
|
each command. Possible return values:
|
|
|
|
=over
|
|
|
|
=item idle
|
|
|
|
Awaiting commands, not in a transaction.
|
|
|
|
=item txn_idle
|
|
|
|
Awaiting commands, inside a transaction.
|
|
|
|
=item txn_done
|
|
|
|
Idle, but a transaction object still exists. The connection is unusable until
|
|
that object goes out of scope.
|
|
|
|
=item txn_error
|
|
|
|
Inside a transaction that is in an error state. The transaction must be rolled
|
|
back in order to recover to a usable state. This happens automatically when the
|
|
transaction object goes out of scope.
|
|
|
|
=item bad
|
|
|
|
Connection is dead or otherwise unusable.
|
|
|
|
=back
|
|
|
|
=item B<< $conn->disconnect >>
|
|
|
|
Close the connection. Any active transactions are rolled back and any further
|
|
attempts to use C<$conn> throw an error.
|
|
|
|
=back
|
|
|
|
=head2 Querying
|
|
|
|
=over
|
|
|
|
=item B<< $conn->exec($sql) >>
|
|
|
|
Execute one or more SQL commands, separated by a semicolon. Returns the number
|
|
of rows affected by the last statement or I<undef> if that information is not
|
|
available for the given command (like `CREATE TABLE`).
|
|
|
|
=item B<< $conn->q($sql, @params) >>
|
|
|
|
Create a new SQL statement with the given C<$sql> string and an optional list
|
|
of bind parameters. C<$sql> can only hold a single statement.
|
|
|
|
Parameters can be referenced from C<$sql> with numbered placeholders, where
|
|
C<$1> refers to the first parameter, C<$2> to the second, etc. Be careful to
|
|
not accidentally interpolate perl's C<$1> and C<$2>. Using a question mark for
|
|
placeholders, as is common with L<DBI>, is not supported. An error is thrown
|
|
when attempting to execute a query where the number of C<@params> does not
|
|
match the number of placeholders in C<$sql>.
|
|
|
|
Note that this method just creates a statement object, the given query is not
|
|
prepared or executed until the appropriate statement methods (see below) are
|
|
used.
|
|
|
|
=back
|
|
|
|
Statement objects returned by C<< $conn->q() >> can be inspected with the
|
|
following two methods:
|
|
|
|
=over
|
|
|
|
=item B<< $st->params >>
|
|
|
|
Returns an arrayref of hashrefs describing each parameter in the given C<$sql>
|
|
string. Each parameter only has a single key for now: C<oid>, indicating the
|
|
type Oid. Example:
|
|
|
|
my $params = $conn->q('SELECT id FROM books WHERE id = $1')->params;
|
|
# $params = [ { oid => 23 } ]
|
|
|
|
my $params = $conn->q('SELECT id FROM books')->params;
|
|
# $params = []
|
|
|
|
I<TODO: Resolve the oid to a more human-readable type>
|
|
|
|
=item B<< $st->columns >>
|
|
|
|
Returns an arrayref of hashrefs describing each column that the statement
|
|
returns.
|
|
|
|
my $cols = $conn->q('SELECT id, title FROM books')->columns;
|
|
# $cols = [
|
|
# { name => 'id', oid => 23 },
|
|
# { name => 'title', oid => 25 },
|
|
# ]
|
|
|
|
|
|
=back
|
|
|
|
The statement can be executed with one of the following methods, depending on
|
|
how you'd like to obtain the results:
|
|
|
|
=over
|
|
|
|
=item B<< $st->exec >>
|
|
|
|
Execute the query and return the number of rows affected. Similar to C<<
|
|
$conn->exec >>.
|
|
|
|
=item B<< $st->val >>
|
|
|
|
Return the first column of the first row. Throws an error if the query does not
|
|
return exactly one column, or if multiple rows are returned. Returns I<undef>
|
|
if no rows are returned or if its value is I<NULL>.
|
|
|
|
=item B<< $st->rowl >>
|
|
|
|
Return the first row as a list. Throws an error if the query does not return
|
|
exactly one row.
|
|
|
|
=item B<< $st->rowa >>
|
|
|
|
Return the first row as an arrayref, equivalent to C<< [$st->rowl] >> but
|
|
probably slightly more efficient.
|
|
|
|
=item B<< $st->rowh >>
|
|
|
|
Return the first row as a hashref. Also throws an error if the query returns
|
|
multiple columns with the same name.
|
|
|
|
=back
|
|
|
|
=head2 Transactions
|
|
|
|
This module provides a convenient and safe API for I<scoped transactions> and
|
|
I<subtransactions>. A new transaction can be started with C<< $conn->txn >>,
|
|
which returns an object that can be used to run commands inside the transaction
|
|
and control its fate. When the object goes out of scope, the transaction is
|
|
automatically rolled back if no explicit C<< $txn->commit >> has been
|
|
performed. Any attempts to run queries on the parent C<< $conn >> object will
|
|
fail while a transaction object is alive.
|
|
|
|
{
|
|
# start a new transaction
|
|
my $txn = $conn->txn;
|
|
|
|
# run queries
|
|
$txn->q('DELETE FROM books WHERE id = $1', 1)->exec;
|
|
|
|
# run commands in a subtransaction
|
|
{
|
|
my $subtxn = $txn->txn;
|
|
# ...
|
|
}
|
|
|
|
# commit
|
|
$txn->commit;
|
|
|
|
# If $txn->commit has not been called, the transaction will be rolled back
|
|
# automatically when it goes out of scope.
|
|
}
|
|
|
|
Transaction methods:
|
|
|
|
=over
|
|
|
|
=item B<< $txn->exec(..) >> and B<< $txn->q(..) >>
|
|
|
|
Run a query inside the transaction. These work the same as the respective
|
|
methods on the parent C<$conn> object.
|
|
|
|
=item B<< $txn->commit >> and B<< $txn->rollback >>
|
|
|
|
Commit or abort the transaction. Any attempts to run queries on this
|
|
transaction object after this call will throw an error.
|
|
|
|
Calling C<rollback> is optional, the transaction is automatically rolled back
|
|
when the object goes out of scope.
|
|
|
|
=item B<< $txn->txn >>
|
|
|
|
Create a subtransaction within the current transaction. A subtransaction works
|
|
exactly the same as a top-level transaction.
|
|
|
|
=item B<< $txn->status >>
|
|
|
|
Like C<< $conn->status >>, but with the following status codes:
|
|
|
|
=over
|
|
|
|
=item idle
|
|
|
|
Current transaction is active and awaiting commands.
|
|
|
|
=item done
|
|
|
|
Current transaction has either been committed or rolled back, further commands
|
|
will throw an error.
|
|
|
|
=item error
|
|
|
|
Current transaction is in error state and must be rolled back.
|
|
|
|
=item txn_idle
|
|
|
|
A subtransaction is active and awaiting commands. The current transaction is
|
|
not usable until the subtransaction goes out of scope.
|
|
|
|
(This status code is also returned when the subtransaction is 'done', the
|
|
current implementation does not track subtransactions that closely)
|
|
|
|
=item txn_error
|
|
|
|
A subtransaction is in error state and awaiting to be rolled back.
|
|
|
|
=item bad
|
|
|
|
Connection is dead or otherwise unusable.
|
|
|
|
=back
|
|
|
|
=back
|
|
|
|
Of course, if you prefer the old-fashioned manual approach to transaction
|
|
handling, that is still available:
|
|
|
|
$conn->exec('BEGIN');
|
|
# We're now inside a transaction
|
|
$conn->exec('COMMIT') or $conn->exec('ROLLBACK');
|
|
|
|
Just don't try to use transaction objects and manual transaction commands at
|
|
the same time, that won't end well.
|
|
|
|
|
|
=head2 Errors
|
|
|
|
I<TODO>
|
|
|
|
=head1 LIMITATIONS
|
|
|
|
=over
|
|
|
|
=item * Does not support older versions of libpq or PostgreSQL. Currently only
|
|
tested with version 17, but versions a bit older than that ought to work fine
|
|
as well. Much older versions will certainly not work fine.
|
|
|
|
=item * (Probably) not thread-safe.
|
|
|
|
=item * Only supports the UTF-8 encoding for string columns (text, char,
|
|
varchar, etc). When using the binary format (the default) this only works if
|
|
your database encoding is UTF-8. Non-UTF-8 databases are still supported with
|
|
the text format by setting `client_encoding=utf8` as part of the connection
|
|
string or by manually switching to it after C<connect()>:
|
|
|
|
my $conn = FU::PG->connect("");
|
|
$conn->exec('SET client_encoding=utf8');
|
|
|
|
(But you're missing out on most features this module has to offer if you're
|
|
stuck with the text format, so L<DBD::Pg> might be a better choice in that
|
|
case)
|
|
|
|
=item * Only works with blocking (synchronous) calls, not very suitable for use
|
|
in asynchronous frameworks unless you know your queries are fast and you have a
|
|
low-latency connection with the Postgres server.
|
|
|
|
=back
|
|
|
|
Missing features (for now): I<pretty much everything>.
|
|
|
|
|
|
=head1 SEE ALSO
|
|
|
|
=over
|
|
|
|
=item L<DBD::Pg>
|
|
|
|
The venerable Postgres driver for DBI. More stable, portable and battle-tested
|
|
than this module, but type conversions may leave things to be desired.
|
|
|
|
=item L<Pg::PQ>
|
|
|
|
A thin wrapper around libpq. Lacks many higher-level conveniences and does not
|
|
support binary transfers (at the time of writing, but then again there's little
|
|
benefit in dealing with the binary format in pure perl anyway).
|
|
|
|
=item L<DBIx::Simple>
|
|
|
|
A popular DBI wrapper with some API conveniences. I may have taken some
|
|
inspiration from it in the design of this module's API.
|
|
|
|
=back
|