pg: Module rename + more docs

This commit is contained in:
Yorhel 2025-02-11 10:54:01 +01:00
parent ccc2f1dbf0
commit 33fe0d98a8
7 changed files with 186 additions and 101 deletions

36
FU.xs
View file

@ -49,15 +49,15 @@ fupg_st * FUPG_ST
INPUT INPUT
FUPG_CONN FUPG_CONN
if (sv_derived_from($arg, \"FU::PG::conn\")) $var = (fupg_conn *)SvIVX(SvRV($arg)); if (sv_derived_from($arg, \"FU::Pg::conn\")) $var = (fupg_conn *)SvIVX(SvRV($arg));
else fu_confess(\"invalid connection object\"); else fu_confess(\"invalid connection object\");
FUPG_TXN FUPG_TXN
if (sv_derived_from($arg, \"FU::PG::txn\")) $var = (fupg_txn *)SvIVX(SvRV($arg)); if (sv_derived_from($arg, \"FU::Pg::txn\")) $var = (fupg_txn *)SvIVX(SvRV($arg));
else fu_confess(\"invalid transaction object\"); else fu_confess(\"invalid transaction object\");
FUPG_ST FUPG_ST
if (sv_derived_from($arg, \"FU::PG::st\")) $var = (fupg_st *)SvIVX(SvRV($arg)); if (sv_derived_from($arg, \"FU::Pg::st\")) $var = (fupg_st *)SvIVX(SvRV($arg));
else fu_confess(\"invalid statement object\"); else fu_confess(\"invalid statement object\");
#" #"
EOT EOT
@ -75,7 +75,7 @@ void json_parse(SV *val, ...)
MODULE = FU PACKAGE = FU::PG MODULE = FU PACKAGE = FU::Pg
void _load_libpq() void _load_libpq()
CODE: CODE:
@ -91,7 +91,7 @@ void connect(const char *pkg, const char *conninfo)
ST(0) = fupg_connect(aTHX_ conninfo); ST(0) = fupg_connect(aTHX_ conninfo);
MODULE = FU PACKAGE = FU::PG::conn MODULE = FU PACKAGE = FU::Pg::conn
void server_version(fupg_conn *c) void server_version(fupg_conn *c)
CODE: CODE:
@ -109,9 +109,9 @@ void status(fupg_conn *c)
void cache(fupg_conn *x, ...) void cache(fupg_conn *x, ...)
ALIAS: ALIAS:
FU::PG::conn::text_params = FUPG_TEXT_PARAMS FU::Pg::conn::text_params = FUPG_TEXT_PARAMS
FU::PG::conn::text_results = FUPG_TEXT_RESULTS FU::Pg::conn::text_results = FUPG_TEXT_RESULTS
FU::PG::conn::text = FUPG_TEXT FU::Pg::conn::text = FUPG_TEXT
CODE: CODE:
FUPG_STFLAGS; FUPG_STFLAGS;
@ -139,7 +139,7 @@ void q(fupg_conn *c, SV *sv, ...)
ST(0) = fupg_q(aTHX_ c, c->stflags, SvPVutf8_nolen(sv), ax, items); ST(0) = fupg_q(aTHX_ c, c->stflags, SvPVutf8_nolen(sv), ax, items);
MODULE = FU PACKAGE = FU::PG::txn MODULE = FU PACKAGE = FU::Pg::txn
void DESTROY(fupg_txn *t) void DESTROY(fupg_txn *t)
CODE: CODE:
@ -147,9 +147,9 @@ void DESTROY(fupg_txn *t)
void cache(fupg_txn *x, ...) void cache(fupg_txn *x, ...)
ALIAS: ALIAS:
FU::PG::txn::text_params = FUPG_TEXT_PARAMS FU::Pg::txn::text_params = FUPG_TEXT_PARAMS
FU::PG::txn::text_results = FUPG_TEXT_RESULTS FU::Pg::txn::text_results = FUPG_TEXT_RESULTS
FU::PG::txn::text = FUPG_TEXT FU::Pg::txn::text = FUPG_TEXT
CODE: CODE:
FUPG_STFLAGS; FUPG_STFLAGS;
@ -183,20 +183,20 @@ void q(fupg_txn *t, SV *sv, ...)
ST(0) = fupg_q(aTHX_ t->conn, t->stflags, SvPVutf8_nolen(sv), ax, items); ST(0) = fupg_q(aTHX_ t->conn, t->stflags, SvPVutf8_nolen(sv), ax, items);
MODULE = FU PACKAGE = FU::PG::st MODULE = FU PACKAGE = FU::Pg::st
void cache(fupg_st *x, ...) void cache(fupg_st *x, ...)
ALIAS: ALIAS:
FU::PG::st::text_params = FUPG_TEXT_PARAMS FU::Pg::st::text_params = FUPG_TEXT_PARAMS
FU::PG::st::text_results = FUPG_TEXT_RESULTS FU::Pg::st::text_results = FUPG_TEXT_RESULTS
FU::PG::st::text = FUPG_TEXT FU::Pg::st::text = FUPG_TEXT
CODE: CODE:
FUPG_STFLAGS; FUPG_STFLAGS;
void params(fupg_st *st) void param_types(fupg_st *st)
CODE: CODE:
FUPG_ST_COOKIE; FUPG_ST_COOKIE;
ST(0) = fupg_st_params(aTHX_ st); ST(0) = fupg_st_param_types(aTHX_ st);
void columns(fupg_st *st) void columns(fupg_st *st)
CODE: CODE:

View file

@ -1,14 +1,14 @@
package FU::PG 0.1; package FU::Pg 0.1;
use v5.36; use v5.36;
use FU::XS; use FU::XS;
_load_libpq(); _load_libpq();
package FU::PG::conn { package FU::Pg::conn {
sub lib_version { FU::PG::lib_version() } sub lib_version { FU::Pg::lib_version() }
}; };
package FU::PG::error { package FU::Pg::error {
use overload '""' => sub($e, @) { $e->{full_message} }; use overload '""' => sub($e, @) { $e->{full_message} };
} }
@ -17,15 +17,16 @@ __END__
=head1 NAME =head1 NAME
FU::PG - Another PostgreSQL client module FU::Pg - The Ultimate (synchronous) Interface to PostgreSQL
=head1 SYNOPSYS =head1 SYNOPSYS
my $conn = FU::PG->connect("dbname=test user=test password=nottest"); my $conn = FU::Pg->connect("dbname=test user=test password=nottest");
$conn->exec('CREATE TABLE books (id SERIAL, title text)'); $conn->exec('CREATE TABLE books (id SERIAL, title text, read bool)');
$conn->q('INSERT INTO books (title) VALUES ($1)', 'Revelation Space')->exec; $conn->q('INSERT INTO books (title) VALUES ($1)', 'Revelation Space')->exec;
$conn->q('INSERT INTO books (title) VALUES ($1)', 'The Invincible')->exec;
for my ($id, $title) ($conn->q('SELECT * FROM books')->flat->@*) { for my ($id, $title) ($conn->q('SELECT * FROM books')->flat->@*) {
print "$id: $title\n"; print "$id: $title\n";
@ -33,33 +34,17 @@ FU::PG - Another PostgreSQL client module
=head1 DESCRIPTION =head1 DESCRIPTION
FU::PG is a PostgreSQL client module that (attempts) to set itself apart from FU::Pg is a client module for PostgreSQL with a convenient high-level API and
the existing alternatives by offering the following features: support for flexible and complex type conversions. This module interfaces
directly with C<libpq>.
=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 =head2 Connection setup
=over =over
=item B<< FU::PG->connect($string) >> =item B<< FU::Pg->connect($string) >>
Connect to the PostgreSQL server and return a new C<FU::PG::conn> object. 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 C<$string> can either be in key=value format or a URI, refer to L<the
PostgreSQL PostgreSQL
documentation|https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING> documentation|https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING>
@ -75,7 +60,7 @@ C<$major * 10000 + $minor>. For example, returns 170002 for PostgreSQL 17.2.
=item B<< $conn->lib_version >> =item B<< $conn->lib_version >>
Returns the libpq version in the same format as the C<server_version> method. Returns the libpq version in the same format as the C<server_version> method.
Also available directly as C<FU::PG::lib_version()>. Also available directly as C<FU::Pg::lib_version()>.
=item B<< $conn->status >> =item B<< $conn->status >>
@ -135,7 +120,7 @@ attempts to use C<$conn> throw an error.
Execute one or more SQL commands, separated by a semicolon. Returns the number 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 of rows affected by the last statement or I<undef> if that information is not
available for the given command (like `CREATE TABLE`). available for the given command (like with C<CREATE TABLE>).
=item B<< $conn->q($sql, @params) >> =item B<< $conn->q($sql, @params) >>
@ -182,19 +167,16 @@ Statement objects can be inspected with the following two methods:
=over =over
=item B<< $st->params >> =item B<< $st->param_types >>
Returns an arrayref of hashrefs describing each parameter in the given C<$sql> Returns an arrayref of integers indicating the type (as I<oid>) of each
string. Each parameter only has a single key for now: C<oid>, indicating the parameter in the given C<$sql> string. Example:
type Oid. Example:
my $params = $conn->q('SELECT id FROM books WHERE id = $1')->params; my $oids = $conn->q('SELECT id FROM books WHERE id = $1 AND title = $2')->param_types;
# $params = [ { oid => 23 } ] # $oids = [23,25]
my $params = $conn->q('SELECT id FROM books')->params; my $oids = $conn->q('SELECT id FROM books')->params;
# $params = [] # $oids = []
I<TODO: Resolve the oid to a more human-readable type>
=item B<< $st->columns >> =item B<< $st->columns >>
@ -220,34 +202,114 @@ how you'd like to obtain the results:
Execute the query and return the number of rows affected. Similar to C<< Execute the query and return the number of rows affected. Similar to C<<
$conn->exec >>. $conn->exec >>.
my $v = $conn->q('UPDATE books SET read = true WHERE id = 1')->exec;
# $v = 1
=item B<< $st->val >> =item B<< $st->val >>
Return the first column of the first row. Throws an error if the query does not 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> 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>. if no rows are returned or if its value is I<NULL>.
my $v = $conn->q('SELECT COUNT(*) FROM books')->val;
# $v = 2
=item B<< $st->rowl >> =item B<< $st->rowl >>
Return the first row as a list. Throws an error if the query does not return Return the first row as a list. Throws an error if the query does not return
exactly one row. exactly one row.
my($id, $title) = $conn->q('SELECT id, title FROM books LIMIT 1')->rowl;
# ($id, $title) = (1, 'Revelation Space');
=item B<< $st->rowa >> =item B<< $st->rowa >>
Return the first row as an arrayref, equivalent to C<< [$st->rowl] >> but Return the first row as an arrayref, equivalent to C<< [$st->rowl] >> but
probably slightly more efficient. might be slightly more efficient.
my $row = $conn->q('SELECT id, title FROM books LIMIT 1')->rowa;
# $row = [1, 'Revelation Space'];
=item B<< $st->rowh >> =item B<< $st->rowh >>
Return the first row as a hashref. Also throws an error if the query returns Return the first row as a hashref. Also throws an error if the query returns
multiple columns with the same name. multiple columns with the same name.
my $row = $conn->q('SELECT id, title FROM books LIMIT 1')->rowh;
# $row = { id => 1, title => 'Revelation Space' };
=item B<< $st->alla >>
Return all rows as an arrayref of arrayrefs.
my $data = $conn->q('SELECT id, title FROM books')->alla;
# $data = [
# [ 1, 'Revelation Space' ],
# [ 2, 'The Invincible' ],
# ];
=item B<< $st->allh >>
Return all rows as an arrayref of hashrefs. Throws an error if the query
returns multiple columns with the same name.
my $data = $conn->q('SELECT id, title FROM books')->allh;
# $data = [
# { id => 1, title => 'Revelation Space' },
# { id => 2, title => 'The Invincible' },
# ];
=item B<< $st->flat >>
Return an arrayref with all rows flattened.
my $data = $conn->q('SELECT id, title FROM books')->flat;
# $data = [
# 1, 'Revelation Space',
# 2, 'The Invincible',
# ];
=item B<< $st->kvv >>
Return a hashref where the first result column is used as key and the second
column as value. If the query only returns a single column, C<true> is used as
value instead. An error is thrown if the query returns 3 or more columns.
my $data = $conn->q('SELECT id, title FROM books')->kvv;
# $data = {
# 1 => 'Revelation Space',
# 2 => 'The Invincible',
# };
=item B<< $st->kva >>
Return a hashref where the first result column is used as key and the remaining
columns are stored as arrayref.
my $data = $conn->q('SELECT id, title, read FROM books')->kva;
# $data = {
# 1 => [ 'Revelation Space', true ],
# 2 => [ 'The Invincible', false ],
# };
=item B<< $st->kvh >>
Return a hashref where the first result column is used as key and the remaining
columns are stored as hashref.
my $data = $conn->q('SELECT id, title, read FROM books')->kvh;
# $data = {
# 1 => { title => 'Revelation Space', read => true },
# 2 => { title => 'The Invincible', read => false },
# };
=back =back
The only time you actually need to assign a statement object to a variable is 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 when you want to inspect C<param_types> or C<columns>, in all other cases you
chain the methods for more concise code. For example: can chain the methods for more concise code. For example:
my @cols = $conn->q('SELECT a, b FROM table')->cache(0)->text->rowa; my $data = $conn->q('SELECT a, b FROM table')->cache(0)->text->alla;
=head2 Transactions =head2 Transactions
@ -319,7 +381,8 @@ connection or any already existing subtransactions.
=item B<< $txn->txn >> =item B<< $txn->txn >>
Create a subtransaction within the current transaction. A subtransaction works Create a subtransaction within the current transaction. A subtransaction works
exactly the same as a top-level transaction. exactly the same as a top-level transaction, except any changes remain
invisible to other sessions until the top-level transaction has been committed.
=item B<< $txn->status >> =item B<< $txn->status >>
@ -371,6 +434,10 @@ Just don't try to use transaction objects and manual transaction commands at
the same time, that won't end well. the same time, that won't end well.
=head2 Formats and Types
I<TODO>
=head2 Errors =head2 Errors
I<TODO> I<TODO>
@ -391,7 +458,7 @@ default, but if this may not be the case in your situation, setting
`client_encoding=utf8` as part of the connection string or manually switching `client_encoding=utf8` as part of the connection string or manually switching
to it after C<connect()> is always safe: to it after C<connect()> is always safe:
my $conn = FU::PG->connect(''); my $conn = FU::Pg->connect('');
$conn->exec('SET client_encoding=utf8'); $conn->exec('SET client_encoding=utf8');
=item * Only works with blocking (synchronous) calls, not very suitable for use =item * Only works with blocking (synchronous) calls, not very suitable for use
@ -400,7 +467,30 @@ low-latency connection with the Postgres server.
=back =back
Missing features (for now): I<pretty much everything>. Missing features:
=over
=item COPY support
I hope to implement this someday.
=item LISTEN support
Would be nice to have, most likely doable without going full async.
=item Asynchronous calls
Probably won't happen. Perl's async story is slightly awkward in general, and
fully supporting async operation might require a fundamental redesign of how
this module works. It certainly won't I<simplify> the implementation.
=item Pipelining
I have some ideas for an API, but doubt I'll ever implement it. Suffers from
the same awkwardness and complexity as asynchronous calls.
=back
=head1 SEE ALSO =head1 SEE ALSO
@ -414,13 +504,12 @@ than this module, but type conversions may leave things to be desired.
=item L<Pg::PQ> =item L<Pg::PQ>
A thin wrapper around libpq. Lacks many higher-level conveniences and does not Thin wrapper around libpq. Lacks many higher-level conveniences and doesn't do
support binary transfers (at the time of writing, but then again there's little any type conversions for you.
benefit in dealing with the binary format in pure perl anyway).
=item L<DBIx::Simple> =item L<DBIx::Simple>
A popular DBI wrapper with some API conveniences. I may have taken some Popular DBI wrapper with some API conveniences. I may have taken some
inspiration from it in the design of this module's API. inspiration from it in the design of this module's API.
=back =back

View file

@ -37,7 +37,7 @@ static SV *fupg_conn_errsv(PGconn *conn, const char *action) {
hv_stores(hv, "action", newSVpv(action, 0)); hv_stores(hv, "action", newSVpv(action, 0));
hv_stores(hv, "severity", newSVpvs("FATAL")); /* Connection-related errors are always fatal */ hv_stores(hv, "severity", newSVpvs("FATAL")); /* Connection-related errors are always fatal */
hv_stores(hv, "message", newSVpv(PQerrorMessage(conn), 0)); hv_stores(hv, "message", newSVpv(PQerrorMessage(conn), 0));
return fu_croak_hv(hv, "FU::PG::error", "FATAL: %s", PQerrorMessage(conn)); return fu_croak_hv(hv, "FU::Pg::error", "FATAL: %s", PQerrorMessage(conn));
} }
__attribute__((noreturn)) __attribute__((noreturn))
@ -89,8 +89,8 @@ static void fupg_result_croak(PGresult *r, const char *action, const char *query
PQclear(r); PQclear(r);
croak_sv(verbose croak_sv(verbose
? fu_croak_hv(hv, "FU::PG::error", "%s", SvPV_nolen(*hv_fetchs(hv, "verbose_message", 0))) ? fu_croak_hv(hv, "FU::Pg::error", "%s", SvPV_nolen(*hv_fetchs(hv, "verbose_message", 0)))
: fu_croak_hv(hv, "FU::PG::error", "%s: %s", : fu_croak_hv(hv, "FU::Pg::error", "%s: %s",
SvPV_nolen(*hv_fetchs(hv, "severity", 0)), SvPV_nolen(*hv_fetchs(hv, "severity", 0)),
SvPV_nolen(*hv_fetchs(hv, "message", 0)) SvPV_nolen(*hv_fetchs(hv, "message", 0))
) )
@ -121,6 +121,7 @@ static void fupg_exec_ok(pTHX_ fupg_conn *c, const char *sql) {
/* Connection & transaction handling */ /* Connection & transaction handling */
static SV *fupg_connect(pTHX_ const char *str) { static SV *fupg_connect(pTHX_ const char *str) {
if (!PQconnectdb) fupg_load();
PGconn *conn = PQconnectdb(str); PGconn *conn = PQconnectdb(str);
if (PQstatus(conn) != CONNECTION_OK) { if (PQstatus(conn) != CONNECTION_OK) {
SV *sv = fupg_conn_errsv(conn, "connect"); SV *sv = fupg_conn_errsv(conn, "connect");
@ -135,7 +136,7 @@ static SV *fupg_connect(pTHX_ const char *str) {
c->ntypes = 0; c->ntypes = 0;
c->types = NULL; c->types = NULL;
fustr_init(&c->buf, NULL, SIZE_MAX); fustr_init(&c->buf, NULL, SIZE_MAX);
return fu_selfobj(c, "FU::PG::conn"); return fu_selfobj(c, "FU::Pg::conn");
} }
static const char *fupg_conn_status(fupg_conn *c) { static const char *fupg_conn_status(fupg_conn *c) {
@ -169,7 +170,7 @@ static SV *fupg_conn_txn(pTHX_ fupg_conn *c) {
t->stflags = c->stflags; t->stflags = c->stflags;
strcpy(t->rollback_cmd, "ROLLBACK"); strcpy(t->rollback_cmd, "ROLLBACK");
SvREFCNT_inc(c->self); SvREFCNT_inc(c->self);
return fu_selfobj(t, "FU::PG::txn"); return fu_selfobj(t, "FU::Pg::txn");
} }
static SV *fupg_txn_txn(pTHX_ fupg_txn *t) { static SV *fupg_txn_txn(pTHX_ fupg_txn *t) {
@ -185,7 +186,7 @@ static SV *fupg_txn_txn(pTHX_ fupg_txn *t) {
n->stflags = t->stflags; n->stflags = t->stflags;
snprintf(n->rollback_cmd, sizeof(n->rollback_cmd), "ROLLBACK TO SAVEPOINT fupg_%"UVuf, cookie); snprintf(n->rollback_cmd, sizeof(n->rollback_cmd), "ROLLBACK TO SAVEPOINT fupg_%"UVuf, cookie);
SvREFCNT_inc(t->self); SvREFCNT_inc(t->self);
return fu_selfobj(n, "FU::PG::txn"); return fu_selfobj(n, "FU::Pg::txn");
} }
static const char *fupg_txn_status(fupg_txn *t) { static const char *fupg_txn_status(fupg_txn *t) {

View file

@ -56,7 +56,7 @@ static SV *fupg_q(pTHX_ fupg_conn *c, int stflags, const char *query, I32 ax, I3
} }
} }
return fu_selfobj(st, "FU::PG::st"); return fu_selfobj(st, "FU::Pg::st");
} }
static void fupg_st_destroy(fupg_st *st) { static void fupg_st_destroy(fupg_st *st) {
@ -124,15 +124,12 @@ static void fupg_st_prepare(pTHX_ fupg_st *st) {
PQclear(sync); PQclear(sync);
} }
static SV *fupg_st_params(pTHX_ fupg_st *st) { static SV *fupg_st_param_types(pTHX_ fupg_st *st) {
fupg_st_prepare(aTHX_ st); fupg_st_prepare(aTHX_ st);
int i, nparams = PQnparams(st->describe); int i, nparams = PQnparams(st->describe);
AV *av = newAV_alloc_x(nparams); AV *av = newAV_alloc_x(nparams);
for (i=0; i<nparams; i++) { for (i=0; i<nparams; i++)
HV *hv = newHV(); av_push_simple(av, newSViv(PQparamtype(st->describe, i)));
hv_stores(hv, "oid", newSViv(PQparamtype(st->describe, i)));
av_push_simple(av, newRV_noinc((SV *)hv));
}
return sv_2mortal(newRV_noinc((SV *)av)); return sv_2mortal(newRV_noinc((SV *)av));
} }
@ -256,10 +253,8 @@ static void fupg_st_check_dupcols(pTHX_ fupg_st *st, int start) {
for (i=start; i<nfields; i++) { for (i=start; i<nfields; i++) {
const char *key = PQfname(r, i); const char *key = PQfname(r, i);
int len = -strlen(key); int len = -strlen(key);
if (hv_exists(hv, key, len)) { if (hv_exists(hv, key, len))
SvREFCNT_dec((SV *)hv);
fu_confess("Query returns multiple columns with the same name ('%s')", key); fu_confess("Query returns multiple columns with the same name ('%s')", key);
}
hv_store(hv, key, len, &PL_sv_yes, 0); hv_store(hv, key, len, &PL_sv_yes, 0);
} }
} }
@ -359,7 +354,7 @@ static SV *fupg_st_allh(pTHX_ fupg_st *st) {
static SV *fupg_st_flat(pTHX_ fupg_st *st) { static SV *fupg_st_flat(pTHX_ fupg_st *st) {
fupg_st_execute(aTHX_ st); fupg_st_execute(aTHX_ st);
int i, j, nrows = PQntuples(st->result); int i, j, nrows = PQntuples(st->result);
AV *av = newAV_alloc_x(nrows); AV *av = newAV_alloc_x(nrows * st->nfields);
SV *sv = sv_2mortal(newRV_noinc((SV *)av)); SV *sv = sv_2mortal(newRV_noinc((SV *)av));
for (i=0; i<nrows; i++) { for (i=0; i<nrows; i++) {
for (j=0; j<st->nfields; j++) for (j=0; j<st->nfields; j++)

View file

@ -1,28 +1,28 @@
use v5.36; use v5.36;
use Test::More; use Test::More;
plan skip_all => $@ if !eval { require FU::PG; } && $@ =~ /Unable to load libpq/; plan skip_all => $@ if !eval { require FU::Pg; } && $@ =~ /Unable to load libpq/;
die $@ if $@; die $@ if $@;
plan skip_all => 'Please set FU_TEST_DB to a PostgreSQL connection string to run these tests' if !$ENV{FU_TEST_DB}; 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) { sub okerr($sev, $act, $msg) {
is ref $@, 'FU::PG::error'; is ref $@, 'FU::Pg::error';
is $@->{severity}, $sev; is $@->{severity}, $sev;
is $@->{action}, $act; is $@->{action}, $act;
like "$@", $msg; like "$@", $msg;
} }
ok !eval { FU::PG->connect("invalid") }; ok !eval { FU::Pg->connect("invalid") };
okerr FATAL => connect => qr/missing "=" after "invalid"/; okerr FATAL => connect => qr/missing "=" after "invalid"/;
ok FU::PG::lib_version() > 100000; ok FU::Pg::lib_version() > 100000;
my $conn = FU::PG->connect($ENV{FU_TEST_DB})->text; my $conn = FU::Pg->connect($ENV{FU_TEST_DB})->text;
$conn->_debug_trace(0); $conn->_debug_trace(0);
is ref $conn, 'FU::PG::conn'; is ref $conn, 'FU::Pg::conn';
ok $conn->server_version > 100000; ok $conn->server_version > 100000;
is $conn->lib_version, FU::PG::lib_version(); is $conn->lib_version, FU::Pg::lib_version();
is $conn->status, 'idle'; is $conn->status, 'idle';
subtest '$conn->exec', sub { subtest '$conn->exec', sub {
@ -35,7 +35,7 @@ subtest '$conn->exec', sub {
ok !defined $conn->exec(''); ok !defined $conn->exec('');
is $conn->exec('SELECT 1'), 1; is $conn->exec('SELECT 1'), 1;
ok !eval { $conn->q('SELEXT')->params; }; ok !eval { $conn->q('SELEXT')->param_types; };
okerr ERROR => prepare => qr/syntax error/; okerr ERROR => prepare => qr/syntax error/;
is $conn->exec('SET client_encoding=utf8'), undef; is $conn->exec('SET client_encoding=utf8'), undef;
@ -45,7 +45,7 @@ subtest '$conn->exec', sub {
subtest '$st prepare & exec', sub { subtest '$st prepare & exec', sub {
{ {
my $st = $conn->q('SELECT 1'); my $st = $conn->q('SELECT 1');
is_deeply $st->params, []; is_deeply $st->param_types, [];
is_deeply $st->columns, [{ name => '?column?', oid => 23 }]; is_deeply $st->columns, [{ name => '?column?', oid => 23 }];
is $conn->exec('SELECT 1 FROM pg_prepared_statements'), 1; is $conn->exec('SELECT 1 FROM pg_prepared_statements'), 1;
is $st->exec, 1; is $st->exec, 1;
@ -55,7 +55,7 @@ subtest '$st prepare & exec', sub {
{ {
my $st = $conn->q("SELECT \$1::int AS a, \$2::char(5) AS \"\x{1F603}\"", 1, 2); 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->param_types, [ 23, 1042 ];
is_deeply $st->columns, [ is_deeply $st->columns, [
{ oid => 23, name => 'a' }, { oid => 23, name => 'a' },
{ oid => 1042, name => "\x{1F603}", typemod => 9 }, { oid => 1042, name => "\x{1F603}", typemod => 9 },
@ -72,7 +72,7 @@ subtest '$st prepare & exec', sub {
like $@, qr/Statement expects 1 bind parameters but 0 were given/; like $@, qr/Statement expects 1 bind parameters but 0 were given/;
# prepare + describe won't let us detect empty queries, hmm... # prepare + describe won't let us detect empty queries, hmm...
is_deeply $conn->q('')->params, []; is_deeply $conn->q('')->param_types, [];
is_deeply $conn->q('')->columns, []; is_deeply $conn->q('')->columns, [];
ok !eval { $conn->q('')->exec; 1 }; ok !eval { $conn->q('')->exec; 1 };

View file

@ -1,11 +1,11 @@
use v5.36; use v5.36;
use Test::More; use Test::More;
plan skip_all => $@ if !eval { require FU::PG; } && $@ =~ /Unable to load libpq/; plan skip_all => $@ if !eval { require FU::Pg; } && $@ =~ /Unable to load libpq/;
die $@ if $@; die $@ if $@;
plan skip_all => 'Please set FU_TEST_DB to a PostgreSQL connection string to run these tests' if !$ENV{FU_TEST_DB}; plan skip_all => 'Please set FU_TEST_DB to a PostgreSQL connection string to run these tests' if !$ENV{FU_TEST_DB};
my $conn = FU::PG->connect($ENV{FU_TEST_DB}); my $conn = FU::Pg->connect($ENV{FU_TEST_DB});
ok !eval { $conn->q('SELECT $1::aclitem', '')->exec; 1 }; ok !eval { $conn->q('SELECT $1::aclitem', '')->exec; 1 };
like $@, qr/Unable to send or receive/; like $@, qr/Unable to send or receive/;

View file

@ -3,11 +3,11 @@ use Test::More;
no warnings 'experimental::builtin'; no warnings 'experimental::builtin';
use builtin qw/true false is_bool created_as_number/; use builtin qw/true false is_bool created_as_number/;
plan skip_all => $@ if !eval { require FU::PG; } && $@ =~ /Unable to load libpq/; plan skip_all => $@ if !eval { require FU::Pg; } && $@ =~ /Unable to load libpq/;
die $@ if $@; die $@ if $@;
plan skip_all => 'Please set FU_TEST_DB to a PostgreSQL connection string to run these tests' if !$ENV{FU_TEST_DB}; plan skip_all => 'Please set FU_TEST_DB to a PostgreSQL connection string to run these tests' if !$ENV{FU_TEST_DB};
my $conn = FU::PG->connect($ENV{FU_TEST_DB}); my $conn = FU::Pg->connect($ENV{FU_TEST_DB});
$conn->_debug_trace(0); $conn->_debug_trace(0);
# TODO: Test behavior of magic bind params # TODO: Test behavior of magic bind params