pg: Rework txn implementation + statement config API

I liked the Perl implementation of transactions, but managing state
between Perl and C is a bit cumbersome, so I've moved the whole thing
into C.

Also added a few statement configuration methods that currently don't do
anything yet.
This commit is contained in:
Yorhel 2025-02-07 18:30:33 +01:00
parent 8f94dd0921
commit 166744dd51
7 changed files with 335 additions and 186 deletions

121
FU/PG.pm
View file

@ -6,61 +6,8 @@ _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} };
}
@ -163,6 +110,16 @@ Connection is dead or otherwise unusable.
=back
=item B<< $conn->cache($enable) >>
=item B<< $conn->text_params($enable) >>
=item B<< $conn->text_results($enable) >>
=item B<< $conn->text($enable) >>
Set the default settings for new statements created with B<< $conn->q() >>.
=item B<< $conn->disconnect >>
Close the connection. Any active transactions are rolled back and any further
@ -198,8 +155,30 @@ used.
=back
Statement objects returned by C<< $conn->q() >> can be inspected with the
following two methods:
Statement objects returned by C<< $conn->q() >> support the following
configuration parameters:
=over
=item B<< $st->cache($enable) >>
Enable or disable caching of the prepared statement for this particular query.
=item B<< $st->text_params($enable) >>
Enable or disable sending bind parameters in the text format.
=item B<< $st->text_results($enable) >>
Enable or disable receiving query results in the text format.
=item B<< $st->text($enable) >>
Shorthand for setting C<text_params> and C<text_results> at the same time.
=back
Statement objects can be inspected with the following two methods:
=over
@ -264,6 +243,13 @@ multiple columns with the same name.
=back
The only time you actually need to assign a statement object to a variable is
when you want to inspect C<params> or C<columns>, in all other cases you can
chain the methods for more concise code. For example:
my @cols = $conn->q('SELECT a, b FROM table')->cache(0)->text->rowa;
=head2 Transactions
This module provides a convenient and safe API for I<scoped transactions> and
@ -298,12 +284,16 @@ Transaction methods:
=over
=item B<< $txn->exec(..) >> and B<< $txn->q(..) >>
=item B<< $txn->exec(..) >>
=item 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 >>
=item B<< $txn->commit >>
=item B<< $txn->rollback >>
Commit or abort the transaction. Any attempts to run queries on this
transaction object after this call will throw an error.
@ -311,6 +301,21 @@ 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->cache($enable) >>
=item B<< $txn->text_params($enable) >>
=item B<< $txn->text_results($enable) >>
=item B<< $txn->text($enable) >>
Set the default settings for new statements created with B<< $txn->q() >>.
These settings are inherited from the main connection when the transaction is
created. Subtransactions inherit these settings from their parent transaction.
Changing these settings within a transaction does not affect the main
connection or any already existing subtransactions.
=item B<< $txn->txn >>
Create a subtransaction within the current transaction. A subtransaction works
@ -386,7 +391,7 @@ 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("");
my $conn = FU::PG->connect("")->text;
$conn->exec('SET client_encoding=utf8');
(But you're missing out on most features this module has to offer if you're