pg: Add transaction & subtransaction support

Was expecting the implementation of this to get overly complicated and
brittle, but using a counter-based cookie and doing parts of it in Perl
made it pretty easy actually.  Pretty happy with how this turned out so
far.

TODO: documentation -.-
This commit is contained in:
Yorhel 2025-02-06 17:38:33 +01:00
parent 9d5905e3b4
commit 171afc0268
5 changed files with 276 additions and 10 deletions

View file

@ -13,6 +13,8 @@ typedef enum {
} ExecStatusType;
typedef enum { PQERRORS_TERSE, PQERRORS_DEFAULT, PQERRORS_VERBOSE, PQERRORS_SQLSTATE } PGVerbosity;
typedef enum { PQSHOW_CONTEXT_NEVER, PQSHOW_CONTEXT_ERRORS, PQSHOW_CONTEXT_ALWAYS } PGContextVisibility;
typedef enum { CONNECTION_OK, CONNECTION_BAD } ConnStatusType; /* There's more, but they're irrelevant to us */
typedef enum { PQTRANS_IDLE, PQTRANS_ACTIVE, PQTRANS_INTRANS, PQTRANS_INERROR, PQTRANS_UNKNOWN } PGTransactionStatusType;
#define PG_DIAG_SEVERITY 'S'
#define PG_DIAG_SEVERITY_NONLOCALIZED 'V'
@ -62,8 +64,9 @@ typedef enum { PQSHOW_CONTEXT_NEVER, PQSHOW_CONTEXT_ERRORS, PQSHOW_CONTEXT_ALWAY
X(PQresultStatus, ExecStatusType, const PGresult *) \
X(PQresultVerboseErrorMessage, char *, const PGresult *, PGVerbosity, PGContextVisibility) \
X(PQserverVersion, int, const PGconn *) \
X(PQstatus, int, const PGconn *) \
X(PQstatus, ConnStatusType, const PGconn *) \
X(PQtrace, void, PGconn *, FILE *) \
X(PQtransactionStatus, PGTransactionStatusType, const PGconn *) \
X(PQuntrace, void, PGconn *)
#define X(n, r, ...) static r (*n)(__VA_ARGS__);

View file

@ -2,6 +2,7 @@ typedef struct {
SV *self;
PGconn *conn;
UV prep_counter;
UV cookie; /* currently active transaction object; 0 = none active */
} fupg_conn;
@ -71,7 +72,7 @@ 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) != 0) {
if (PQstatus(conn) != CONNECTION_OK) {
SV *sv = fupg_conn_errsv(conn, "connect");
PQfinish(conn);
croak_sv(sv);
@ -82,6 +83,17 @@ static SV *fupg_connect(pTHX_ const char *str) {
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_destroy(fupg_conn *c) {
PQfinish(c->conn);
safefree(c);
@ -112,6 +124,26 @@ static SV *fupg_exec(pTHX_ fupg_conn *c, const char *sql) {
return ret;
}
/* Validate a FU::PG::txn object and extract the connection */
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)) croak("Invalid attempt to run a query on a transaction that has already finished");
if (c->cookie != SvUV(*v)) croak("Invalid cross-transaction operation");
return c;
invalid:
croak("invalid transaction object");
}
/* Read a Perl value from a PGresult.
* Currently assumes text format and just creates a PV. */
static SV *fupg_val(pTHX_ const PGresult *r, int row, int col) {
@ -123,6 +155,7 @@ typedef struct {
/* Set in $conn->q() */
SV *self;
fupg_conn *conn; /* has a refcnt on conn->self */
UV cookie;
char *query;
SV **bind;
int bindn;
@ -139,6 +172,7 @@ typedef struct {
static SV *fupg_q(pTHX_ fupg_conn *c, const char *query, I32 ax, I32 argc) {
fupg_st *st = safecalloc(1, sizeof(fupg_st));
st->conn = c;
st->cookie = c->cookie;
SvREFCNT_inc(c->self);
st->query = savepv(query);
@ -249,7 +283,6 @@ static void fupg_st_execute(pTHX_ fupg_st *st) {
PGresult *r = PQexecPrepared(st->conn->conn, st->name, st->paramn, (const char * const*)st->param, NULL, NULL, 0);
if (!r) fupg_conn_croak(st->conn , "exec");
switch (PQresultStatus(r)) {
case PGRES_EMPTY_QUERY:
case PGRES_COMMAND_OK:
case PGRES_TUPLES_OK: break;
default: fupg_result_croak(r, "exec", st->query);
@ -334,5 +367,4 @@ static void fupg_st_destroy(fupg_st *st) {
/* TODO: $st->alla, allh, flat, kvv, kva, kvh */
/* TODO: Prepared statement caching */
/* TODO: Transactions */
/* TODO: Binary format fetching & type handling */