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:
parent
8f94dd0921
commit
166744dd51
7 changed files with 335 additions and 186 deletions
112
FU.xs
112
FU.xs
|
|
@ -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
121
FU/PG.pm
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
257
c/pgconn.c
257
c/pgconn.c
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue