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) { is ref $@, 'FU::Pg::error'; is $@->{severity}, $sev; is $@->{action}, $act; like "$@", $msg; } ok !eval { FU::Pg->connect("invalid") }; okerr FATAL => connect => qr/missing "=" after "invalid"/; ok FU::Pg::lib_version() > 100000; my $conn = FU::Pg->connect($ENV{FU_TEST_DB})->text; $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'); }; okerr FATAL => exec => qr/unexpected status code/; ok !eval { $conn->exec('SELEXT'); }; okerr ERROR => exec => qr/syntax error/; ok !defined $conn->exec(''); is $conn->exec('SELECT 1'), 1; ok !eval { $conn->q('SELEXT')->param_types; }; okerr ERROR => prepare => qr/syntax error/; is $conn->exec('SET client_encoding=utf8'), undef; }; subtest '$st prepare & exec', sub { { my $st = $conn->q('SELECT 1'); is_deeply $st->param_types, []; 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->param_types, [ 23, 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; ok !eval { $conn->q('SELECT 1', 1)->exec; 1 }; like $@, qr/Statement expects 0 bind parameters but 1 were given/; ok !eval { $conn->q('SELECT $1')->exec; 1 }; like $@, qr/Statement expects 1 bind parameters but 0 were given/; # prepare + describe won't let us detect empty queries, hmm... is_deeply $conn->q('')->param_types, []; 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 { ok !eval { $conn->q('SELECT')->val; 1 }; like $@, qr/on query returning no data/; ok !eval { $conn->q('SELECT 1, 2')->val; 1 }; like $@, qr/on query returning more than one column/; ok !eval { $conn->q('SELECT 1 UNION SELECT 2')->val; 1 }; like $@, qr/on query returning more than one row/; ok !defined $conn->q('SELECT 1 WHERE false')->val; ok !defined $conn->q('SELECT null')->val; is $conn->q('SELECT $1::text', "\x{1F603}")->val, "\x{1F603}"; }; subtest '$st->rowl', sub { ok !eval { $conn->q('SELECT 1 WHERE false')->rowl; 1 }; like $@, qr/on query returning zero rows/; ok !eval { $conn->q('SELECT 1 UNION SELECT 2')->rowl; 1 }; like $@, qr/on query returning more than one row/; ok !eval { $conn->q('SELEXT')->rowl; 1; }; is scalar $conn->q('SELECT')->rowl, 0; is scalar $conn->q('SELECT 1, 2')->rowl, 2; is_deeply [$conn->q('SELECT')->rowl], []; is_deeply [$conn->q('SELECT 1, null')->rowl], [1, undef]; is_deeply [$conn->q('SELECT 1, $1', undef)->rowl], [1, undef]; is_deeply [$conn->q('SELECT 1, $1::int', undef)->text_params(0)->rowl], [1, undef]; }; subtest '$st->rowa', sub { ok !eval { $conn->q('SELECT 1 WHERE false')->rowa; 1 }; like $@, qr/on query returning zero rows/; ok !eval { $conn->q('SELECT 1 UNION SELECT 2')->rowa; 1 }; like $@, qr/on query returning more than one row/; ok !eval { $conn->q('SELEXT')->rowa; 1; }; is_deeply $conn->q('SELECT')->rowa, []; is_deeply $conn->q('SELECT 1, 2')->rowa, [1, 2]; is_deeply $conn->q('SELECT 1, null')->rowa, [1, undef]; is_deeply $conn->q('SELECT 1, $1', undef)->rowa, [1, undef]; is_deeply $conn->q('SELECT 1, $1::int', undef)->text_params(0)->rowa, [1, undef]; }; subtest '$st->rowh', sub { ok !eval { $conn->q('SELECT 1 WHERE false')->rowh; 1 }; like $@, qr/on query returning zero rows/; ok !eval { $conn->q('SELECT 1 UNION SELECT 2')->rowh; 1 }; like $@, qr/on query returning more than one row/; ok !eval { $conn->q('SELECT 1 as a, 2 as a')->rowh; 1 }; like $@, qr/Query returns multiple columns with the same name/; ok !eval { $conn->q('SELEXT')->rowh; 1; }; is_deeply $conn->q('SELECT')->rowh, {}; is_deeply $conn->q('SELECT 1 as a, 2 as b')->rowh, {a => 1, b => 2}; is_deeply $conn->q('SELECT 1 as a, null as b')->rowh, {a => 1, b => undef}; is_deeply $conn->q('SELECT 1 as a, $1::int as b', undef)->rowh, {a => 1, b => undef}; }; subtest '$st->alla', sub { is_deeply $conn->q('SELECT 1 WHERE false')->alla, []; is_deeply $conn->q('SELECT')->alla, [[]]; is_deeply $conn->q('SELECT 1')->alla, [[1]]; is_deeply $conn->q('SELECT 1, null UNION ALL SELECT NULL, 2')->alla, [[1,undef],[undef,2]]; }; subtest '$st->allh', sub { ok !eval { $conn->q('SELECT 1 as a, 2 as a')->allh; 1 }; like $@, qr/Query returns multiple columns with the same name/; is_deeply $conn->q('SELECT 1 WHERE false')->allh, []; is_deeply $conn->q('SELECT')->allh, [{}]; is_deeply $conn->q('SELECT 1 a')->allh, [{a=>1}]; is_deeply $conn->q('SELECT 1 a, null b UNION ALL SELECT NULL, 2')->allh, [{a=>1,b=>undef},{a=>undef,b=>2}]; }; subtest '$st->flat', sub { is_deeply $conn->q('SELECT 1 WHERE false')->flat, []; is_deeply $conn->q('SELECT')->flat, []; is_deeply $conn->q('SELECT 1')->flat, [1]; is_deeply $conn->q('SELECT 1, null UNION ALL SELECT NULL, 2')->flat, [1,undef,undef,2]; }; subtest '$st->kvv', sub { ok !eval { $conn->q('SELECT')->kvv; 1; }; like $@, qr/returning no data/; ok !eval { $conn->q('SELECT 1, 2, 3')->kvv; 1; }; like $@, qr/returning more than two columns/; ok !eval { $conn->q('SELECT 1 UNION ALL SELECT 1')->kvv; 1; }; like $@, qr/is duplicated/; is_deeply $conn->q('SELECT 1 WHERE false')->kvv, {}; is_deeply $conn->q('SELECT 1')->kvv, {1=>1}; is_deeply $conn->q('SELECT 1, null UNION ALL SELECT 3, 2')->kvv, {1=>undef,3=>2}; }; subtest '$st->kva', sub { ok !eval { $conn->q('SELECT')->kva; 1; }; like $@, qr/returning no data/; ok !eval { $conn->q('SELECT 1 UNION ALL SELECT 1')->kva; 1; }; like $@, qr/is duplicated/; is_deeply $conn->q('SELECT 1 WHERE false')->kva, {}; is_deeply $conn->q('SELECT 1')->kva, {1=>[]}; is_deeply $conn->q("SELECT 1, null, 'hi' UNION ALL SELECT 3, 2, 'ok'")->kva, {1=>[undef,'hi'], 3=>[2, 'ok']}; }; subtest '$st->kvh', sub { ok !eval { $conn->q('SELECT')->kvh; 1; }; like $@, qr/returning no data/; ok !eval { $conn->q('SELECT 1 UNION ALL SELECT 1')->kvh; 1; }; like $@, qr/is duplicated/; ok !eval { $conn->q('SELECT 1, 2, 3')->kvh; 1; }; like $@, qr/Query returns multiple columns with the same name/; is_deeply $conn->q('SELECT 1 WHERE false')->kvh, {}; is_deeply $conn->q('SELECT 1')->kvh, {1=>{}}; is_deeply $conn->q("SELECT 1 as a , null as a, 'hi' as b UNION ALL SELECT 3, 2, 'ok'")->kvh, {1=>{a=>undef,b=>'hi'}, 3=>{a=>2,b=>'ok'}}; }; 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 operation on the top-level connection/; ok !eval { $conn->q('SELECT 1'); 1 }; like $@, qr/Invalid operation on the top-level connection/; ok !eval { $conn->txn; 1 }; like $@, qr/Invalid operation on the top-level connection/; $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/Invalid operation on a transaction that has already been marked as done/; ok !eval { $txn->commit; 1 }; like $@, qr/Invalid operation on a transaction that has already been marked as done/; ok !eval { $txn->txn; 1 }; like $@, qr/Invalid operation on a transaction that has already been marked as done/; ok !eval { $txn->exec('select 1'); 1 }; like $@, qr/Invalid operation on a transaction that has already been marked as done/; ok !eval { $txn->q('select 1'); 1 }; like $@, qr/Invalid operation on a transaction that has already been marked as done/; ok !eval { $conn->exec('SELECT 1'); 1 }; like $@, qr/Invalid operation on the top-level connection/; } 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 operation on 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"); undef $conn; # statement keeps the connection alive is $st->val, 1; } done_testing;