From 33fe0d98a851eecbf23f3a88b41c8fc0a4e93d91 Mon Sep 17 00:00:00 2001 From: Yorhel Date: Tue, 11 Feb 2025 10:54:01 +0100 Subject: [PATCH] pg: Module rename + more docs --- FU.xs | 36 ++++----- FU/{PG.pm => Pg.pm} | 191 ++++++++++++++++++++++++++++++++------------ c/pgconn.c | 13 +-- c/pgst.c | 17 ++-- t/pgconnect.t | 22 ++--- t/pgtypes-dynamic.t | 4 +- t/pgtypes.t | 4 +- 7 files changed, 186 insertions(+), 101 deletions(-) rename FU/{PG.pm => Pg.pm} (67%) diff --git a/FU.xs b/FU.xs index 98a0063..7cd2ba6 100644 --- a/FU.xs +++ b/FU.xs @@ -49,15 +49,15 @@ fupg_st * FUPG_ST INPUT 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\"); 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\"); 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\"); #" EOT @@ -75,7 +75,7 @@ void json_parse(SV *val, ...) -MODULE = FU PACKAGE = FU::PG +MODULE = FU PACKAGE = FU::Pg void _load_libpq() CODE: @@ -91,7 +91,7 @@ void connect(const char *pkg, const char *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) CODE: @@ -109,9 +109,9 @@ void status(fupg_conn *c) void cache(fupg_conn *x, ...) ALIAS: - FU::PG::conn::text_params = FUPG_TEXT_PARAMS - FU::PG::conn::text_results = FUPG_TEXT_RESULTS - FU::PG::conn::text = FUPG_TEXT + FU::Pg::conn::text_params = FUPG_TEXT_PARAMS + FU::Pg::conn::text_results = FUPG_TEXT_RESULTS + FU::Pg::conn::text = FUPG_TEXT CODE: 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); -MODULE = FU PACKAGE = FU::PG::txn +MODULE = FU PACKAGE = FU::Pg::txn void DESTROY(fupg_txn *t) CODE: @@ -147,9 +147,9 @@ void DESTROY(fupg_txn *t) void cache(fupg_txn *x, ...) ALIAS: - FU::PG::txn::text_params = FUPG_TEXT_PARAMS - FU::PG::txn::text_results = FUPG_TEXT_RESULTS - FU::PG::txn::text = FUPG_TEXT + FU::Pg::txn::text_params = FUPG_TEXT_PARAMS + FU::Pg::txn::text_results = FUPG_TEXT_RESULTS + FU::Pg::txn::text = FUPG_TEXT CODE: 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); -MODULE = FU PACKAGE = FU::PG::st +MODULE = FU PACKAGE = FU::Pg::st void cache(fupg_st *x, ...) ALIAS: - FU::PG::st::text_params = FUPG_TEXT_PARAMS - FU::PG::st::text_results = FUPG_TEXT_RESULTS - FU::PG::st::text = FUPG_TEXT + FU::Pg::st::text_params = FUPG_TEXT_PARAMS + FU::Pg::st::text_results = FUPG_TEXT_RESULTS + FU::Pg::st::text = FUPG_TEXT CODE: FUPG_STFLAGS; -void params(fupg_st *st) +void param_types(fupg_st *st) CODE: FUPG_ST_COOKIE; - ST(0) = fupg_st_params(aTHX_ st); + ST(0) = fupg_st_param_types(aTHX_ st); void columns(fupg_st *st) CODE: diff --git a/FU/PG.pm b/FU/Pg.pm similarity index 67% rename from FU/PG.pm rename to FU/Pg.pm index 8c9729e..33055c4 100644 --- a/FU/PG.pm +++ b/FU/Pg.pm @@ -1,14 +1,14 @@ -package FU::PG 0.1; +package FU::Pg 0.1; use v5.36; use FU::XS; _load_libpq(); -package FU::PG::conn { - sub lib_version { FU::PG::lib_version() } +package FU::Pg::conn { + sub lib_version { FU::Pg::lib_version() } }; -package FU::PG::error { +package FU::Pg::error { use overload '""' => sub($e, @) { $e->{full_message} }; } @@ -17,15 +17,16 @@ __END__ =head1 NAME -FU::PG - Another PostgreSQL client module +FU::Pg - The Ultimate (synchronous) Interface to PostgreSQL =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)', 'The Invincible')->exec; for my ($id, $title) ($conn->q('SELECT * FROM books')->flat->@*) { print "$id: $title\n"; @@ -33,33 +34,17 @@ FU::PG - Another PostgreSQL client module =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 +FU::Pg is a client module for PostgreSQL with a convenient high-level API and +support for flexible and complex type conversions. This module interfaces +directly with C. =head2 Connection setup =over -=item B<< FU::PG->connect($string) >> +=item B<< FU::Pg->connect($string) >> -Connect to the PostgreSQL server and return a new C object. +Connect to the PostgreSQL server and return a new C object. C<$string> can either be in key=value format or a URI, refer to L @@ -75,7 +60,7 @@ 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 method. -Also available directly as C. +Also available directly as C. =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 of rows affected by the last statement or I if that information is not -available for the given command (like `CREATE TABLE`). +available for the given command (like with C). =item B<< $conn->q($sql, @params) >> @@ -182,19 +167,16 @@ Statement objects can be inspected with the following two methods: =over -=item B<< $st->params >> +=item B<< $st->param_types >> -Returns an arrayref of hashrefs describing each parameter in the given C<$sql> -string. Each parameter only has a single key for now: C, indicating the -type Oid. Example: +Returns an arrayref of integers indicating the type (as I) of each +parameter in the given C<$sql> string. Example: - my $params = $conn->q('SELECT id FROM books WHERE id = $1')->params; - # $params = [ { oid => 23 } ] + my $oids = $conn->q('SELECT id FROM books WHERE id = $1 AND title = $2')->param_types; + # $oids = [23,25] - my $params = $conn->q('SELECT id FROM books')->params; - # $params = [] - -I + my $oids = $conn->q('SELECT id FROM books')->params; + # $oids = [] =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<< $conn->exec >>. + my $v = $conn->q('UPDATE books SET read = true WHERE id = 1')->exec; + # $v = 1 + =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 if no rows are returned or if its value is I. + my $v = $conn->q('SELECT COUNT(*) FROM books')->val; + # $v = 2 + =item B<< $st->rowl >> Return the first row as a list. Throws an error if the query does not return 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 >> 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 >> Return the first row as a hashref. Also throws an error if the query returns 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 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 The only time you actually need to assign a statement object to a variable is -when you want to inspect C or C, in all other cases you can -chain the methods for more concise code. For example: +when you want to inspect C or C, 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; + my $data = $conn->q('SELECT a, b FROM table')->cache(0)->text->alla; =head2 Transactions @@ -319,7 +381,8 @@ connection or any already existing subtransactions. =item B<< $txn->txn >> 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 >> @@ -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. +=head2 Formats and Types + +I + =head2 Errors I @@ -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 to it after C is always safe: - my $conn = FU::PG->connect(''); + my $conn = FU::Pg->connect(''); $conn->exec('SET client_encoding=utf8'); =item * Only works with blocking (synchronous) calls, not very suitable for use @@ -400,7 +467,30 @@ low-latency connection with the Postgres server. =back -Missing features (for now): I. +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 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 @@ -414,13 +504,12 @@ than this module, but type conversions may leave things to be desired. =item L -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). +Thin wrapper around libpq. Lacks many higher-level conveniences and doesn't do +any type conversions for you. =item L -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. =back diff --git a/c/pgconn.c b/c/pgconn.c index 9b472f2..7cffa9c 100644 --- a/c/pgconn.c +++ b/c/pgconn.c @@ -37,7 +37,7 @@ static SV *fupg_conn_errsv(PGconn *conn, const char *action) { hv_stores(hv, "action", newSVpv(action, 0)); hv_stores(hv, "severity", newSVpvs("FATAL")); /* Connection-related errors are always fatal */ 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)) @@ -89,8 +89,8 @@ static void fupg_result_croak(PGresult *r, const char *action, const char *query PQclear(r); 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: %s", + ? 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", SvPV_nolen(*hv_fetchs(hv, "severity", 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 */ static SV *fupg_connect(pTHX_ const char *str) { + if (!PQconnectdb) fupg_load(); PGconn *conn = PQconnectdb(str); if (PQstatus(conn) != CONNECTION_OK) { SV *sv = fupg_conn_errsv(conn, "connect"); @@ -135,7 +136,7 @@ static SV *fupg_connect(pTHX_ const char *str) { c->ntypes = 0; c->types = NULL; 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) { @@ -169,7 +170,7 @@ static SV *fupg_conn_txn(pTHX_ fupg_conn *c) { t->stflags = c->stflags; strcpy(t->rollback_cmd, "ROLLBACK"); 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) { @@ -185,7 +186,7 @@ static SV *fupg_txn_txn(pTHX_ fupg_txn *t) { n->stflags = t->stflags; snprintf(n->rollback_cmd, sizeof(n->rollback_cmd), "ROLLBACK TO SAVEPOINT fupg_%"UVuf, cookie); 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) { diff --git a/c/pgst.c b/c/pgst.c index 952d72a..4fc099b 100644 --- a/c/pgst.c +++ b/c/pgst.c @@ -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) { @@ -124,15 +124,12 @@ static void fupg_st_prepare(pTHX_ fupg_st *st) { 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); int i, nparams = PQnparams(st->describe); AV *av = newAV_alloc_x(nparams); - for (i=0; idescribe, i))); - av_push_simple(av, newRV_noinc((SV *)hv)); - } + for (i=0; idescribe, i))); 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; iresult); - AV *av = newAV_alloc_x(nrows); + AV *av = newAV_alloc_x(nrows * st->nfields); SV *sv = sv_2mortal(newRV_noinc((SV *)av)); for (i=0; infields; j++) diff --git a/t/pgconnect.t b/t/pgconnect.t index f620095..2903195 100644 --- a/t/pgconnect.t +++ b/t/pgconnect.t @@ -1,28 +1,28 @@ use v5.36; 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 $@; 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 ref $@, 'FU::Pg::error'; is $@->{severity}, $sev; is $@->{action}, $act; like "$@", $msg; } -ok !eval { FU::PG->connect("invalid") }; +ok !eval { FU::Pg->connect("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); -is ref $conn, 'FU::PG::conn'; +is ref $conn, 'FU::Pg::conn'; 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'; subtest '$conn->exec', sub { @@ -35,7 +35,7 @@ subtest '$conn->exec', sub { ok !defined $conn->exec(''); 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/; is $conn->exec('SET client_encoding=utf8'), undef; @@ -45,7 +45,7 @@ subtest '$conn->exec', sub { subtest '$st prepare & exec', sub { { my $st = $conn->q('SELECT 1'); - is_deeply $st->params, []; + 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; @@ -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); - is_deeply $st->params, [ { oid => 23 }, { oid => 1042 } ]; + is_deeply $st->param_types, [ 23, 1042 ]; is_deeply $st->columns, [ { oid => 23, name => 'a' }, { 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/; # 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, []; ok !eval { $conn->q('')->exec; 1 }; diff --git a/t/pgtypes-dynamic.t b/t/pgtypes-dynamic.t index 30bc4fa..e859474 100644 --- a/t/pgtypes-dynamic.t +++ b/t/pgtypes-dynamic.t @@ -1,11 +1,11 @@ use v5.36; 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 $@; 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 }; like $@, qr/Unable to send or receive/; diff --git a/t/pgtypes.t b/t/pgtypes.t index 18e7803..175a266 100644 --- a/t/pgtypes.t +++ b/t/pgtypes.t @@ -3,11 +3,11 @@ use Test::More; no warnings 'experimental::builtin'; 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 $@; 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); # TODO: Test behavior of magic bind params