pg: Add prepared statement caching

The tests are not as thourough as I would like. There's many ways to
mess this up.

I was initially planning to drop the ref on the prepared statement
immediately after executing the query, so that the $st object can be
kept around for introspection without consuming excess resources.
Unfortunately, PQcopyResult does not copy over information about bind
parameters, so we need another way to keep that information alive. I
ended up going for the simple solution: keep the ref on the prepared
statement...
This commit is contained in:
Yorhel 2025-02-12 17:08:22 +01:00
parent 87d99e412b
commit 1f7e2de9a0
4 changed files with 190 additions and 10 deletions

View file

@ -11,7 +11,8 @@ typedef struct {
/* Set during prepare */
int prepared;
char name[32];
PGresult *describe;
fupg_prep *prep;
PGresult *describe; /* shared with prep->describe if prep is set */
/* Set during execute */
int nfields;
@ -50,8 +51,8 @@ static SV *fupg_q(pTHX_ fupg_conn *c, int stflags, const char *query, I32 ax, I3
st->bind = safemalloc((argc-2) * sizeof(SV *));
I32 i;
for (i=2; i < argc; i++) {
SvGETMAGIC(ST(i));
st->bind[st->nbind] = SvREFCNT_inc(ST(i));
st->bind[st->nbind] = newSV(0);
sv_setsv(st->bind[st->nbind], ST(i));
st->nbind++;
}
}
@ -61,8 +62,13 @@ static SV *fupg_q(pTHX_ fupg_conn *c, int stflags, const char *query, I32 ax, I3
static void fupg_st_destroy(fupg_st *st) {
int i;
/* Ignore failure, this is just a best-effort attempt to free up resources on the backend */
if (st->prepared) PQclear(PQclosePrepared(st->conn->conn, st->name));
if (st->prep) {
fupg_prepared_unref(st->conn, st->prep);
} else if (st->prepared) {
PQclear(st->describe);
PQclear(PQclosePrepared(st->conn->conn, st->name));
}
safefree(st->query);
for (i=0; i < st->nbind; i++) SvREFCNT_dec(st->bind[i]);
@ -73,7 +79,6 @@ static void fupg_st_destroy(fupg_st *st) {
if (st->recv) for (i=0; i<st->nfields; i++) fupg_tio_free(st->recv + i);
fupg_tio_free(&st->send);
safefree(st->recv);
PQclear(st->describe);
PQclear(st->result);
SvREFCNT_dec(st->conn->self);
safefree(st);
@ -83,9 +88,18 @@ static void fupg_st_prepare(pTHX_ fupg_st *st) {
if (st->describe) return;
if (st->prepared) fu_confess("invalid attempt to re-prepare invalid statement");
/* TODO: This is where we check for any cached prepared statements */
if (st->stflags & FUPG_CACHE)
st->prep = fupg_prepared_ref(st->conn, st->query);
if (st->prep && st->prep->describe) {
snprintf(st->name, sizeof(st->name), "fupg%"UVuf, st->prep->name);
st->describe = st->prep->describe;
st->prepared = 1;
return;
}
snprintf(st->name, sizeof(st->name), "fupg%"UVuf, ++st->conn->prep_counter);
st->conn->prep_counter++;
if (st->prep) st->prep->name = st->conn->prep_counter;
snprintf(st->name, sizeof(st->name), "fupg%"UVuf, st->conn->prep_counter);
/* Send prepare + describe in a pipeline to avoid a double round-trip with the server */
PQenterPipelineMode(st->conn->conn);
@ -116,6 +130,7 @@ static void fupg_st_prepare(pTHX_ fupg_st *st) {
PQclear(sync);
fupg_result_croak(desc, "prepare", st->query);
}
if (st->prep) st->prep->describe = desc;
st->describe = desc;
if (!sync) fupg_conn_croak(st->conn , "prepare");