pg: Add transaction & subtransaction support
Was expecting the implementation of this to get overly complicated and brittle, but using a counter-based cookie and doing parts of it in Perl made it pretty easy actually. Pretty happy with how this turned out so far. TODO: documentation -.-
This commit is contained in:
parent
9d5905e3b4
commit
171afc0268
5 changed files with 276 additions and 10 deletions
146
t/pgconnect.t
146
t/pgconnect.t
|
|
@ -2,6 +2,7 @@ use v5.36;
|
|||
use Test::More;
|
||||
|
||||
plan skip_all => $@ if !eval { require FU::PG; } && $@ =~ /Unable to load libpq/;
|
||||
die $@ if $@;
|
||||
plan skip_all => 'Please set FU_TEST_DB to a PostgreSQL connection string to run these tests' if !$ENV{FU_TEST_DB};
|
||||
|
||||
sub okerr($sev, $act, $msg) {
|
||||
|
|
@ -22,6 +23,7 @@ $conn->_debug_trace(0);
|
|||
is ref $conn, 'FU::PG::conn';
|
||||
ok $conn->server_version > 100000;
|
||||
is $conn->lib_version, FU::PG::lib_version();
|
||||
is $conn->status, 'idle';
|
||||
|
||||
subtest '$conn->exec', sub {
|
||||
ok !eval { $conn->exec('COPY (SELECT 1) TO STDOUT'); };
|
||||
|
|
@ -35,6 +37,8 @@ subtest '$conn->exec', sub {
|
|||
|
||||
ok !eval { $conn->q('SELEXT')->params; };
|
||||
okerr ERROR => prepare => qr/syntax error/;
|
||||
|
||||
is $conn->exec('SET client_encoding=utf8'), undef;
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -45,6 +49,18 @@ subtest '$st prepare & exec', sub {
|
|||
is_deeply $st->columns, [{ name => '?column?', oid => 23 }];
|
||||
is $conn->exec('SELECT 1 FROM pg_prepared_statements'), 1;
|
||||
is $st->exec, 1;
|
||||
ok !eval { $st->exec; 1 };
|
||||
like $@, qr/Invalid attempt to execute statement multiple times/;
|
||||
}
|
||||
|
||||
{
|
||||
my $st = $conn->q("SELECT \$1::int AS a, \$2::char(5) AS \"\x{1F603}\"", 1, 2);
|
||||
is_deeply $st->params, [ { oid => 23 }, { oid => 1042 } ];
|
||||
is_deeply $st->columns, [
|
||||
{ oid => 23, name => 'a' },
|
||||
{ oid => 1042, name => "\x{1F603}", typemod => 9 },
|
||||
];
|
||||
is $st->exec, 1;
|
||||
}
|
||||
|
||||
is $conn->exec('SELECT 1 FROM pg_prepared_statements'), 0;
|
||||
|
|
@ -54,6 +70,15 @@ subtest '$st prepare & exec', sub {
|
|||
|
||||
ok !eval { $conn->q('SELECT $1')->exec; 1 };
|
||||
okerr ERROR => exec => qr/bind message supplies 0 parameters, but prepared statement/;
|
||||
|
||||
# prepare + describe won't let us detect empty queries, hmm...
|
||||
is_deeply $conn->q('')->params, [];
|
||||
is_deeply $conn->q('')->columns, [];
|
||||
|
||||
ok !eval { $conn->q('')->exec; 1 };
|
||||
okerr FATAL => exec => qr/unexpected status code/;
|
||||
|
||||
is $conn->q('SET client_encoding=utf8')->exec, undef;
|
||||
};
|
||||
|
||||
subtest '$st->val', sub {
|
||||
|
|
@ -112,14 +137,123 @@ subtest '$st->rowh', sub {
|
|||
is_deeply $conn->q('SELECT 1 as a, 2 as b')->rowh, {a => 1, b => 2};
|
||||
};
|
||||
|
||||
subtest 'txn', sub {
|
||||
$conn->exec('CREATE TEMPORARY TABLE fupg_tst (id int)');
|
||||
$conn->txn->exec('INSERT INTO fupg_tst VALUES (1)'); # rolled back
|
||||
is $conn->q('SELECT COUNT(*) FROM fupg_tst')->val, 0;
|
||||
|
||||
my $st = $conn->q('SELECT COUNT(*) FROM fupg_tst');
|
||||
my $sst;
|
||||
{
|
||||
my $txn = $conn->txn;
|
||||
is $conn->status, 'txn_idle';
|
||||
is $txn->status, 'idle';
|
||||
|
||||
ok !eval { $st->exec; 1 };
|
||||
like $@, qr/Invalid cross-transaction/;
|
||||
|
||||
ok !eval { $conn->exec('SELECT 1'); 1 };
|
||||
like $@, qr/Invalid attempt to run a query/;
|
||||
ok !eval { $conn->q('SELECT 1'); 1 };
|
||||
like $@, qr/Invalid attempt to run a query/;
|
||||
ok !eval { $conn->txn; 1 };
|
||||
like $@, qr/Invalid attempt to run a query/;
|
||||
|
||||
$txn->exec('INSERT INTO fupg_tst VALUES (1)');
|
||||
$sst = $txn->q('SELECT 1');
|
||||
|
||||
is $conn->status, 'txn_idle';
|
||||
is $txn->status, 'idle';
|
||||
$txn->commit;
|
||||
is $conn->status, 'txn_done';
|
||||
is $txn->status, 'done';
|
||||
|
||||
ok !eval { $txn->rollback; 1 };
|
||||
like $@, qr/Unable to rollback/;
|
||||
ok !eval { $txn->commit; 1 };
|
||||
like $@, qr/Unable to commit/;
|
||||
ok !eval { $txn->txn; 1 };
|
||||
like $@, qr/Unable to create/;
|
||||
ok !eval { $txn->exec('select 1'); 1 };
|
||||
like $@, qr/Invalid attempt to run a query/;
|
||||
ok !eval { $txn->q('select 1'); 1 };
|
||||
like $@, qr/Invalid attempt to run a query/;
|
||||
|
||||
ok !eval { $conn->exec('SELECT 1'); 1 };
|
||||
like $@, qr/Invalid attempt to run a query/;
|
||||
}
|
||||
is $conn->status, 'idle';
|
||||
is $st->val, 1;
|
||||
ok !eval { $sst->exec; 1 };
|
||||
like $@, qr/Invalid cross-transaction/;
|
||||
|
||||
{
|
||||
my $txn = $conn->txn;
|
||||
ok !eval { $txn->exec('SELEXT'); 1 }; # puts txn in error state
|
||||
is $conn->status, 'txn_error';
|
||||
is $txn->status, 'error';
|
||||
ok !eval { $txn->exec('SELECT 1'); 1 };
|
||||
like $@, qr/current transaction is aborted/;
|
||||
|
||||
$txn->rollback;
|
||||
is $conn->status, 'txn_done';
|
||||
is $txn->status, 'done';
|
||||
}
|
||||
ok $conn->exec('SELECT 1');
|
||||
|
||||
{
|
||||
my $txn = $conn->txn;
|
||||
my $st = $txn->q('SELECT count(*) FROM fupg_tst WHERE id = 2');
|
||||
{
|
||||
my $sub = $txn->txn;
|
||||
is $conn->status, 'txn_idle';
|
||||
is $txn->status, 'txn_idle';
|
||||
is $sub->status, 'idle';
|
||||
|
||||
$sub->exec('INSERT INTO fupg_tst VALUES (2)');
|
||||
ok !eval { $sub->exec('SELEXT'); 1 };
|
||||
|
||||
ok !eval { $txn->rollback; 1 };
|
||||
like $@, qr/Invalid cross-transaction/;
|
||||
|
||||
is $conn->status, 'txn_error';
|
||||
is $txn->status, 'txn_error';
|
||||
is $sub->status, 'error';
|
||||
}
|
||||
is $conn->status, 'txn_idle';
|
||||
is $txn->status, 'idle';
|
||||
is $st->val, 0;
|
||||
|
||||
$st = $txn->q('SELECT count(*) FROM fupg_tst WHERE id = 2');
|
||||
{
|
||||
my $sub = $txn->txn;
|
||||
$sub->exec('INSERT INTO fupg_tst VALUES (2)');
|
||||
$sub->commit;
|
||||
is $conn->status, 'txn_idle';
|
||||
is $txn->status, 'txn_idle'; # No way to tell that it's actually done
|
||||
is $sub->status, 'done';
|
||||
}
|
||||
is $st->val, 1;
|
||||
}
|
||||
is $conn->status, 'idle';
|
||||
|
||||
{
|
||||
my $txn = $conn->txn;
|
||||
my $sub = $txn->txn;
|
||||
undef $txn; # sub keeps a ref on $txn
|
||||
is $sub->status, 'idle';
|
||||
is $conn->status, 'txn_idle';
|
||||
$sub->exec('INSERT INTO fupg_tst VALUES (3)');
|
||||
$sub->commit;
|
||||
}
|
||||
# We didn't commit $txn, so $sub got aborted as well
|
||||
is $conn->q('SELECT count(*) FROM fupg_tst WHERE id = 3')->val, 0;
|
||||
};
|
||||
|
||||
{
|
||||
my $st = $conn->q("SELECT \$1::int AS a, \$2::char(5) AS \"\x{1F603}\"");
|
||||
my $st = $conn->q("SELECT 1");
|
||||
undef $conn; # statement keeps the connection alive
|
||||
is_deeply $st->params, [ { oid => 23 }, { oid => 1042 } ];
|
||||
is_deeply $st->columns, [
|
||||
{ oid => 23, name => 'a' },
|
||||
{ oid => 1042, name => "\x{1F603}", typemod => 9 },
|
||||
];
|
||||
is $st->val, 1;
|
||||
}
|
||||
|
||||
done_testing;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue