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

112
FU.xs
View file

@ -16,13 +16,21 @@
#define FUPG_CONN_COOKIE \ #define FUPG_CONN_COOKIE \
if (c->cookie) fu_confess("Invalid attempt to run a query on the top-level connection while a transaction object exists") if (c->cookie) fu_confess("Invalid operation on the top-level connection while a transaction object exists")
#define FUPG_TXN_COOKIE \
if (!t->cookie) fu_confess("Invalid operation on a transaction that has already been marked as done"); \
if (t->cookie != t->conn->cookie) fu_confess("Invalid operation on transaction while a subtransaction object exists")
#define FUPG_ST_COOKIE \ #define FUPG_ST_COOKIE \
if (st->cookie != st->conn->cookie) fu_confess("Invalid cross-transaction operation on statement object") if (st->cookie != st->conn->cookie) fu_confess("Invalid cross-transaction operation on statement object")
typedef fupg_conn *fupg_txn; #define FUPG_STFLAGS do {\
if (!ix) ix = FUPG_CACHE;\
if (items == 1 || SvTRUE(ST(1))) x->stflags |= ix; \
else x->stflags &= ~ix; \
XSRETURN(1); \
} while(0)
MODULE = FU MODULE = FU
@ -32,7 +40,7 @@ PROTOTYPES: DISABLE
TYPEMAP: <<EOT TYPEMAP: <<EOT
TYPEMAP TYPEMAP
fupg_conn * FUPG_CONN fupg_conn * FUPG_CONN
fupg_txn FUPG_TXN fupg_txn * FUPG_TXN
fupg_st * FUPG_ST fupg_st * FUPG_ST
INPUT INPUT
@ -41,7 +49,8 @@ FUPG_CONN
else fu_confess(\"invalid connection object\"); else fu_confess(\"invalid connection object\");
FUPG_TXN FUPG_TXN
$var = fupg_get_transaction(aTHX_ $arg); if (sv_derived_from($arg, \"FU::PG::txn\")) $var = (fupg_txn *)SvIVX(SvRV($arg));
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));
@ -94,19 +103,30 @@ void _debug_trace(fupg_conn *c, bool on)
else PQuntrace(c->conn); else PQuntrace(c->conn);
ST(0) = c->self; ST(0) = c->self;
void _set_cookie(fupg_conn *c, UV cookie)
CODE:
c->cookie = cookie;
UV _get_cookie(fupg_conn *c)
CODE:
RETVAL = c->cookie;
OUTPUT:
RETVAL
void status(fupg_conn *c) void status(fupg_conn *c)
CODE: CODE:
ST(0) = sv_2mortal(newSVpv(fupg_status(c), 0)); ST(0) = sv_2mortal(newSVpv(fupg_conn_status(c), 0));
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
CODE:
FUPG_STFLAGS;
void disconnect(fupg_conn *c)
CODE:
fupg_conn_disconnect(c);
void DESTROY(fupg_conn *c)
CODE:
fupg_conn_destroy(c);
void txn(fupg_conn *c)
CODE:
FUPG_CONN_COOKIE;
ST(0) = fupg_conn_txn(c);
void exec(fupg_conn *c, SV *sv) void exec(fupg_conn *c, SV *sv)
CODE: CODE:
@ -116,34 +136,62 @@ void exec(fupg_conn *c, SV *sv)
void q(fupg_conn *c, SV *sv, ...) void q(fupg_conn *c, SV *sv, ...)
CODE: CODE:
FUPG_CONN_COOKIE; FUPG_CONN_COOKIE;
ST(0) = fupg_q(aTHX_ c, SvPVutf8_nolen(sv), ax, items); ST(0) = fupg_q(aTHX_ c, c->stflags, SvPVutf8_nolen(sv), ax, items);
void disconnect(fupg_conn *c)
CODE:
fupg_disconnect(c);
void DESTROY(fupg_conn *c)
CODE:
fupg_destroy(c);
MODULE = FU PACKAGE = FU::PG::txn MODULE = FU PACKAGE = FU::PG::txn
void exec(fupg_txn c, SV *sv) void DESTROY(fupg_txn *t)
CODE: CODE:
ST(0) = fupg_exec(aTHX_ c, SvPVutf8_nolen(sv)); fupg_txn_destroy(t);
void q(fupg_txn c, SV *sv, ...) 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
CODE: CODE:
ST(0) = fupg_q(aTHX_ c, SvPVutf8_nolen(sv), ax, items); FUPG_STFLAGS;
void status(fupg_txn *t)
CODE:
ST(0) = sv_2mortal(newSVpv(fupg_txn_status(t), 0));
void txn(fupg_txn *t)
CODE:
FUPG_TXN_COOKIE;
ST(0) = fupg_txn_txn(t);
void commit(fupg_txn *t)
CODE:
FUPG_TXN_COOKIE;
fupg_txn_commit(t);
void rollback(fupg_txn *t)
CODE:
FUPG_TXN_COOKIE;
fupg_txn_rollback(t);
void exec(fupg_txn *t, SV *sv)
CODE:
FUPG_TXN_COOKIE;
ST(0) = fupg_exec(aTHX_ t->conn, SvPVutf8_nolen(sv));
void q(fupg_txn *t, SV *sv, ...)
CODE:
FUPG_TXN_COOKIE;
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 text_results(fupg_st *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
CODE: CODE:
st->text_results = items == 1 || SvTRUE(ST(1)); FUPG_STFLAGS;
XSRETURN(1);
void params(fupg_st *st) void params(fupg_st *st)
CODE: CODE:

121
FU/PG.pm
View file

@ -6,61 +6,8 @@ _load_libpq();
package FU::PG::conn { package FU::PG::conn {
sub lib_version { FU::PG::lib_version() } 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 { package FU::PG::error {
use overload '""' => sub($e, @) { $e->{full_message} }; use overload '""' => sub($e, @) { $e->{full_message} };
} }
@ -163,6 +110,16 @@ Connection is dead or otherwise unusable.
=back =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 >> =item B<< $conn->disconnect >>
Close the connection. Any active transactions are rolled back and any further Close the connection. Any active transactions are rolled back and any further
@ -198,8 +155,30 @@ used.
=back =back
Statement objects returned by C<< $conn->q() >> can be inspected with the Statement objects returned by C<< $conn->q() >> support the following
following two methods: 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 =over
@ -264,6 +243,13 @@ multiple columns with the same name.
=back =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 =head2 Transactions
This module provides a convenient and safe API for I<scoped transactions> and This module provides a convenient and safe API for I<scoped transactions> and
@ -298,12 +284,16 @@ Transaction methods:
=over =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 Run a query inside the transaction. These work the same as the respective
methods on the parent C<$conn> object. 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 Commit or abort the transaction. Any attempts to run queries on this
transaction object after this call will throw an error. 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 Calling C<rollback> is optional, the transaction is automatically rolled back
when the object goes out of scope. 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 >> =item B<< $txn->txn >>
Create a subtransaction within the current transaction. A subtransaction works 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 the text format by setting `client_encoding=utf8` as part of the connection
string or by manually switching to it after C<connect()>: 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'); $conn->exec('SET client_encoding=utf8');
(But you're missing out on most features this module has to offer if you're (But you're missing out on most features this module has to offer if you're

View file

@ -3,6 +3,7 @@ use Config;
os_unsupported if $^O eq 'MSWin32'; # I don't know on which OS'es the code will work exactly, but this one I can easily rule out. os_unsupported if $^O eq 'MSWin32'; # I don't know on which OS'es the code will work exactly, but this one I can easily rule out.
os_unsupported if $Config{ivsize} < 8; os_unsupported if $Config{ivsize} < 8;
os_unsupported if $Config{byteorder} != 12345678; # pgtypes.c assumes we're little-endian
os_unsupported if $Config{usequadmath}; os_unsupported if $Config{usequadmath};
WriteMakefile( WriteMakefile(

View file

@ -1,11 +1,11 @@
/* Because I don't know how to use sv_setref_pv() correctly. */ /* Because I don't know how to use sv_setref_pv() correctly. */
static SV *fupg_selfobj_(pTHX_ SV **self, void *obj, const char *klass) { static SV *fu_selfobj_(pTHX_ SV **self, void *obj, const char *klass) {
*self = newSViv(PTR2IV(obj)); *self = newSViv(PTR2IV(obj));
return sv_bless(sv_2mortal(newRV_noinc(*self)), gv_stashpv(klass, GV_ADD)); return sv_bless(sv_2mortal(newRV_noinc(*self)), gv_stashpv(klass, GV_ADD));
} }
/* Write a blessed SV to obj->self and returns a mortal ref to it */ /* Write a blessed SV to obj->self and returns a mortal ref to it */
#define fupg_selfobj(obj, klass) fupg_selfobj_(aTHX_ &((obj)->self), obj, klass) #define fu_selfobj(obj, klass) fu_selfobj_(aTHX_ &((obj)->self), obj, klass)

View file

@ -1,11 +1,55 @@
#define FUPG_CACHE 1
#define FUPG_TEXT_PARAMS 2
#define FUPG_TEXT_RESULTS 4
#define FUPG_TEXT (FUPG_TEXT_PARAMS|FUPG_TEXT_RESULTS)
typedef struct { typedef struct {
SV *self; SV *self;
PGconn *conn; PGconn *conn;
UV prep_counter; UV prep_counter;
UV cookie_counter;
UV cookie; /* currently active transaction object; 0 = none active */ UV cookie; /* currently active transaction object; 0 = none active */
int stflags;
} fupg_conn; } fupg_conn;
struct fupg_txn {
SV *self;
struct fupg_txn *parent;
fupg_conn *conn;
UV cookie; /* 0 means done */
int stflags;
char rollback_cmd[64];
};
typedef struct fupg_txn fupg_txn;
typedef struct {
/* Set in $conn->q() */
SV *self; /* (unused, but whatever) */
fupg_conn *conn; /* has a refcnt on conn->self */
UV cookie;
char *query;
SV **bind;
int nbind;
int stflags;
/* Set during prepare */
int prepared;
char name[32];
PGresult *describe;
/* Set during execute */
int nparam;
int nfields;
char **param;
const fupg_type **recv;
void **recvctx;
PGresult *result;
} fupg_st;
/* Utilities */
static SV *fupg_conn_errsv(PGconn *conn, const char *action) { static SV *fupg_conn_errsv(PGconn *conn, const char *action) {
dTHX; dTHX;
HV *hv = newHV(); HV *hv = newHV();
@ -15,12 +59,14 @@ static SV *fupg_conn_errsv(PGconn *conn, const char *action) {
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))
static void fupg_conn_croak(fupg_conn *c, const char *action) { static void fupg_conn_croak(fupg_conn *c, const char *action) {
dTHX; dTHX;
croak_sv(fupg_conn_errsv(c->conn, action)); croak_sv(fupg_conn_errsv(c->conn, action));
} }
/* Takes ownership of the PGresult and croaks. */ /* Takes ownership of the PGresult and croaks. */
__attribute__((noreturn))
static void fupg_result_croak(PGresult *r, const char *action, const char *query) { static void fupg_result_croak(PGresult *r, const char *action, const char *query) {
dTHX; dTHX;
HV *hv = newHV(); HV *hv = newHV();
@ -70,40 +116,6 @@ static void fupg_result_croak(PGresult *r, const char *action, const char *query
); );
} }
static SV *fupg_connect(pTHX_ const char *str) {
PGconn *conn = PQconnectdb(str);
if (PQstatus(conn) != CONNECTION_OK) {
SV *sv = fupg_conn_errsv(conn, "connect");
PQfinish(conn);
croak_sv(sv);
}
fupg_conn *c = safecalloc(1, sizeof(fupg_conn));
c->conn = conn;
return fupg_selfobj(c, "FU::PG::conn");
}
static const char *fupg_status(fupg_conn *c) {
if (PQstatus(c->conn) == CONNECTION_BAD) return "bad";
switch (PQtransactionStatus(c->conn)) {
case PQTRANS_IDLE: return c->cookie ? "txn_done" : "idle";
case PQTRANS_ACTIVE: return "active"; /* can't happen, we don't do async */
case PQTRANS_INTRANS: return "txn_idle";
case PQTRANS_INERROR: return "txn_error";
default: return "unknown";
}
}
static void fupg_disconnect(fupg_conn *c) {
PQfinish(c->conn);
c->conn = NULL;
}
static void fupg_destroy(fupg_conn *c) {
PQfinish(c->conn);
safefree(c);
}
static SV *fupg_exec_result(pTHX_ PGresult *r) { static SV *fupg_exec_result(pTHX_ PGresult *r) {
SV *ret = &PL_sv_undef; SV *ret = &PL_sv_undef;
char *tup = PQcmdTuples(r); char *tup = PQcmdTuples(r);
@ -115,6 +127,130 @@ static SV *fupg_exec_result(pTHX_ PGresult *r) {
return ret; return ret;
} }
static void fupg_exec_ok(pTHX_ fupg_conn *c, const char *sql) {
PGresult *r = PQexec(c->conn, sql);
if (!r) fupg_conn_croak(c, "exec");
if (PQresultStatus(r) != PGRES_COMMAND_OK) fupg_result_croak(r, "exec", sql);
PQclear(r);
}
/* Connection & transaction handling */
static SV *fupg_connect(pTHX_ const char *str) {
PGconn *conn = PQconnectdb(str);
if (PQstatus(conn) != CONNECTION_OK) {
SV *sv = fupg_conn_errsv(conn, "connect");
PQfinish(conn);
croak_sv(sv);
}
fupg_conn *c = safecalloc(1, sizeof(fupg_conn));
c->conn = conn;
return fu_selfobj(c, "FU::PG::conn");
}
static const char *fupg_conn_status(fupg_conn *c) {
if (PQstatus(c->conn) == CONNECTION_BAD) return "bad";
switch (PQtransactionStatus(c->conn)) {
case PQTRANS_IDLE: return c->cookie ? "txn_done" : "idle";
case PQTRANS_ACTIVE: return "active"; /* can't happen, we don't do async */
case PQTRANS_INTRANS: return "txn_idle";
case PQTRANS_INERROR: return "txn_error";
default: return "unknown";
}
}
static void fupg_conn_disconnect(fupg_conn *c) {
PQfinish(c->conn);
c->conn = NULL;
}
static void fupg_conn_destroy(fupg_conn *c) {
PQfinish(c->conn);
safefree(c);
}
static SV *fupg_conn_txn(pTHX_ fupg_conn *c) {
fupg_exec_ok(c, "BEGIN");
fupg_txn *t = safecalloc(1, sizeof(fupg_txn));
t->conn = c;
t->cookie = c->cookie = ++c->cookie_counter;
t->stflags = c->stflags;
strcpy(t->rollback_cmd, "ROLLBACK");
SvREFCNT_inc(c->self);
return fu_selfobj(t, "FU::PG::txn");
}
static SV *fupg_txn_txn(pTHX_ fupg_txn *t) {
char cmd[64];
UV cookie = ++t->conn->cookie_counter;
snprintf(cmd, sizeof(cmd), "SAVEPOINT fupg_%"UVuf, cookie);
fupg_exec_ok(t->conn, cmd);
fupg_txn *n = safecalloc(1, sizeof(fupg_txn));
n->conn = t->conn;
n->parent = t;
n->cookie = t->conn->cookie = cookie;
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");
}
static const char *fupg_txn_status(fupg_txn *t) {
if (PQstatus(t->conn->conn) == CONNECTION_BAD) return "bad";
if (!t->cookie) return "done";
int a = t->cookie == t->conn->cookie;
switch (PQtransactionStatus(t->conn->conn)) {
case PQTRANS_IDLE: return "done";
case PQTRANS_ACTIVE: return "active";
case PQTRANS_INTRANS: return a ? "idle" : "txn_idle";
case PQTRANS_INERROR: return a ? "error" : "txn_error";
default: return "unknown";
}
}
static void fupg_txn_commit(pTHX_ fupg_txn *t) {
char cmd[64];
if (t->parent) snprintf(cmd, sizeof(cmd), "RELEASE SAVEPOINT fupg_%"UVuf, t->cookie);
else strcpy(cmd, "COMMIT");
t->cookie = 0;
fupg_exec_ok(t->conn, cmd);
}
static void fupg_txn_rollback(pTHX_ fupg_txn *t) {
t->cookie = 0;
fupg_exec_ok(t->conn, t->rollback_cmd);
}
static void fupg_txn_destroy(pTHX_ fupg_txn *t) {
if (t->cookie) {
PGresult *r = PQexec(t->conn->conn, t->rollback_cmd);
/* 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. */
if (!r || PQresultStatus(r) != PGRES_COMMAND_OK)
fupg_conn_disconnect(t->conn);
PQclear(r);
}
if (t->parent) {
t->conn->cookie = t->parent->cookie;
SvREFCNT_dec(t->parent->self);
} else {
t->conn->cookie = 0;
SvREFCNT_dec(t->conn->self);
}
safefree(t);
}
/* Querying */
static SV *fupg_exec(pTHX_ fupg_conn *c, const char *sql) { static SV *fupg_exec(pTHX_ fupg_conn *c, const char *sql) {
PGresult *r = PQexec(c->conn, sql); PGresult *r = PQexec(c->conn, sql);
if (!r) fupg_conn_croak(c, "exec"); if (!r) fupg_conn_croak(c, "exec");
@ -129,54 +265,11 @@ static SV *fupg_exec(pTHX_ fupg_conn *c, const char *sql) {
return ret; return ret;
} }
/* Validate a FU::PG::txn object and extract the connection */ static SV *fupg_q(pTHX_ fupg_conn *c, int stflags, const char *query, I32 ax, I32 argc) {
static fupg_conn *fupg_get_transaction(pTHX_ SV *sv) {
if (!sv_derived_from(sv, "FU::PG::txn")) goto invalid;
sv = SvRV(sv);
if (SvTYPE(sv) != SVt_PVAV) goto invalid;
AV *av = (AV *)sv;
SV **v = av_fetch(av, 0, 0);
if (!v || !*v) goto invalid;
fupg_conn *c = (fupg_conn *)SvIVX(SvRV(*v));
v = av_fetch(av, 1, 0);
if (!v || !*v) goto invalid;
if (!SvOK(*v)) fu_confess("Invalid attempt to run a query on a transaction that has already finished");
if (c->cookie != SvUV(*v)) fu_confess("Invalid cross-transaction operation");
return c;
invalid:
fu_confess("invalid transaction object");
}
typedef struct {
/* Set in $conn->q() */
SV *self;
fupg_conn *conn; /* has a refcnt on conn->self */
UV cookie;
char *query;
SV **bind;
int nbind;
bool text_params;
bool text_results;
/* Set during prepare */
int prepared;
char name[32];
PGresult *describe;
/* Set during execute */
int nparam;
int nfields;
char **param;
const fupg_type **recv;
void **recvctx;
PGresult *result;
} fupg_st;
static SV *fupg_q(pTHX_ fupg_conn *c, const char *query, I32 ax, I32 argc) {
fupg_st *st = safecalloc(1, sizeof(fupg_st)); fupg_st *st = safecalloc(1, sizeof(fupg_st));
st->conn = c; st->conn = c;
st->cookie = c->cookie; st->cookie = c->cookie;
st->text_params = st->text_results = true; /* TODO: default to false */ st->stflags = stflags;
SvREFCNT_inc(c->self); SvREFCNT_inc(c->self);
st->query = savepv(query); st->query = savepv(query);
@ -190,7 +283,7 @@ static SV *fupg_q(pTHX_ fupg_conn *c, const char *query, I32 ax, I32 argc) {
} }
} }
return fupg_selfobj(st, "FU::PG::st"); return fu_selfobj(st, "FU::PG::st");
} }
static void fupg_st_prepare(pTHX_ fupg_st *st) { static void fupg_st_prepare(pTHX_ fupg_st *st) {
@ -287,7 +380,7 @@ static void fupg_st_execute(pTHX_ fupg_st *st) {
* improvement */ * improvement */
PGresult *r = PQexecPrepared(st->conn->conn, PGresult *r = PQexecPrepared(st->conn->conn,
st->name, st->nparam, (const char * const*)st->param, st->name, st->nparam, (const char * const*)st->param,
NULL, NULL, st->text_results ? 0 : 1); NULL, NULL, st->stflags & FUPG_TEXT_RESULTS ? 0 : 1);
if (!r) fupg_conn_croak(st->conn , "exec"); if (!r) fupg_conn_croak(st->conn , "exec");
switch (PQresultStatus(r)) { switch (PQresultStatus(r)) {
case PGRES_COMMAND_OK: case PGRES_COMMAND_OK:
@ -300,7 +393,7 @@ static void fupg_st_execute(pTHX_ fupg_st *st) {
st->recv = safecalloc(st->nfields, sizeof(*st->recv)); st->recv = safecalloc(st->nfields, sizeof(*st->recv));
st->recvctx = safecalloc(st->nfields, sizeof(*st->recvctx)); st->recvctx = safecalloc(st->nfields, sizeof(*st->recvctx));
for (i=0; i<st->nfields; i++) { for (i=0; i<st->nfields; i++) {
st->recv[i] = fupg_type_lookup(st->text_results ? 0 : PQftype(r, i)); st->recv[i] = fupg_type_lookup(st->stflags & FUPG_TEXT_RESULTS ? 0 : PQftype(r, i));
if (!st->recv[i]) if (!st->recv[i])
fu_confess("Unable to receive query results of type %u", PQftype(r, i)); fu_confess("Unable to receive query results of type %u", PQftype(r, i));
} }

View file

@ -3,7 +3,9 @@
typedef void (*fupg_send_fn)(pTHX_ SV *, fustr *, void *); typedef void (*fupg_send_fn)(pTHX_ SV *, fustr *, void *);
/* Receive function, takes a binary string and should return a Perl value. /* Receive function, takes a binary string and should return a Perl value.
* libpq guarantees that the given buffer is aligned to MAXIMUM_ALIGNOF. */ * libpq guarantees that the given buffer is aligned to MAXIMUM_ALIGNOF.
* For fixed-length types, the recv function is only called after verifying
* that the input buffer has the correct length. */
typedef SV *(*fupg_recv_fn)(pTHX_ const char *, int, void *); typedef SV *(*fupg_recv_fn)(pTHX_ const char *, int, void *);
typedef struct { typedef struct {

View file

@ -17,7 +17,7 @@ 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}); 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';
@ -153,11 +153,11 @@ subtest 'txn', sub {
like $@, qr/Invalid cross-transaction/; like $@, qr/Invalid cross-transaction/;
ok !eval { $conn->exec('SELECT 1'); 1 }; ok !eval { $conn->exec('SELECT 1'); 1 };
like $@, qr/Invalid attempt to run a query/; like $@, qr/Invalid operation on the top-level connection/;
ok !eval { $conn->q('SELECT 1'); 1 }; ok !eval { $conn->q('SELECT 1'); 1 };
like $@, qr/Invalid attempt to run a query/; like $@, qr/Invalid operation on the top-level connection/;
ok !eval { $conn->txn; 1 }; ok !eval { $conn->txn; 1 };
like $@, qr/Invalid attempt to run a query/; like $@, qr/Invalid operation on the top-level connection/;
$txn->exec('INSERT INTO fupg_tst VALUES (1)'); $txn->exec('INSERT INTO fupg_tst VALUES (1)');
$sst = $txn->q('SELECT 1'); $sst = $txn->q('SELECT 1');
@ -169,18 +169,18 @@ subtest 'txn', sub {
is $txn->status, 'done'; is $txn->status, 'done';
ok !eval { $txn->rollback; 1 }; ok !eval { $txn->rollback; 1 };
like $@, qr/Unable to rollback/; like $@, qr/Invalid operation on a transaction that has already been marked as done/;
ok !eval { $txn->commit; 1 }; ok !eval { $txn->commit; 1 };
like $@, qr/Unable to commit/; like $@, qr/Invalid operation on a transaction that has already been marked as done/;
ok !eval { $txn->txn; 1 }; ok !eval { $txn->txn; 1 };
like $@, qr/Unable to create/; like $@, qr/Invalid operation on a transaction that has already been marked as done/;
ok !eval { $txn->exec('select 1'); 1 }; ok !eval { $txn->exec('select 1'); 1 };
like $@, qr/Invalid attempt to run a query/; like $@, qr/Invalid operation on a transaction that has already been marked as done/;
ok !eval { $txn->q('select 1'); 1 }; ok !eval { $txn->q('select 1'); 1 };
like $@, qr/Invalid attempt to run a query/; like $@, qr/Invalid operation on a transaction that has already been marked as done/;
ok !eval { $conn->exec('SELECT 1'); 1 }; ok !eval { $conn->exec('SELECT 1'); 1 };
like $@, qr/Invalid attempt to run a query/; like $@, qr/Invalid operation on the top-level connection/;
} }
is $conn->status, 'idle'; is $conn->status, 'idle';
is $st->val, 1; is $st->val, 1;
@ -214,7 +214,7 @@ subtest 'txn', sub {
ok !eval { $sub->exec('SELEXT'); 1 }; ok !eval { $sub->exec('SELEXT'); 1 };
ok !eval { $txn->rollback; 1 }; ok !eval { $txn->rollback; 1 };
like $@, qr/Invalid cross-transaction/; like $@, qr/Invalid operation on transaction/;
is $conn->status, 'txn_error'; is $conn->status, 'txn_error';
is $txn->status, 'txn_error'; is $txn->status, 'txn_error';