Skip to content

Commit 8982351

Browse files
outtersgdevnexen
authored andcommitted
Pdo\Pgsql: Fix getColumnMeta() for GH-15287 Pdo\Pgsql::setAttribute(PDO::ATTR_PREFETCH, 0)
As stated in the UPGRADING, using the passthrough ("single-row") mode of libpq (introduced in #15287) forbids passing a new query while the current one's results have not been entirely consumed. … But I didn't notice that ext/pdo_pgsql internally used new queries to fetch metadata (example use case: a call to getColumnMeta() while fetch()ing row by row will interleave the getColumnMeta()-triggered internal query to the database, with the results fetching for the user-called query). This PR makes those internal calls return NULL for non-essential metadata, instead of letting libpq abort the user-called query. It moreover includes a small tweak to table oid-to-name translation, with a 1-slot cache. This may by chance allow the internal call to return something instead of NULL, but it will nonetheless avoid 30 server calls to get the table name of 30 columns of the same table. optimize calls to foreach(columns of the same table) getColumnMeta() - each call queried the DB to know the name associated with the table's OID: cache the result between two calls - make pdo_pgsql_translate_oid_to_table higher-level, with the last parameter being the handle instead of the raw connection; thus the statement is cleaner, letting the handle do all memory handling on the table oid-to-name translation cache (which by the way is a driver feature more than a statement one) close GH-16249
1 parent 81100a7 commit 8982351

File tree

5 files changed

+109
-26
lines changed

5 files changed

+109
-26
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ PHP NEWS
77
with a given skeleton, locale, collapse type and identity fallback.
88
(BogdanUngureanu)
99

10+
- PDO_PGSQL:
11+
. Fixed Pdo\Pgsql::getColumnMeta() when Pdo\Pgsql::setAttribute(PDO::ATTR_PREFETCH, 0).
12+
(outtersg)
13+
1014
<<< NOTE: Insert NEWS from last stable release here prior to actual release! >>>

ext/pdo_pgsql/pgsql_driver.c

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "pgsql_driver_arginfo.h"
3737

3838
static bool pgsql_handle_in_transaction(pdo_dbh_t *dbh);
39+
void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode);
3940

4041
static char * _pdo_pgsql_trim_message(const char *message, int persistent)
4142
{
@@ -109,6 +110,37 @@ int _pdo_pgsql_error(pdo_dbh_t *dbh, pdo_stmt_t *stmt, int errcode, const char *
109110
}
110111
/* }}} */
111112

113+
static zend_always_inline void pgsql_finish_running_stmt(pdo_pgsql_db_handle *H)
114+
{
115+
if (H->running_stmt) {
116+
pgsql_stmt_finish(H->running_stmt, 0);
117+
}
118+
}
119+
120+
static zend_always_inline void pgsql_discard_running_stmt(pdo_pgsql_db_handle *H)
121+
{
122+
if (H->running_stmt) {
123+
pgsql_stmt_finish(H->running_stmt, FIN_DISCARD);
124+
}
125+
126+
PGresult *pgsql_result;
127+
bool first = true;
128+
while ((pgsql_result = PQgetResult(H->server))) {
129+
/* We should not arrive here, where libpq has a result to deliver without us
130+
* having registered a running statement:
131+
* every result discarding should go through the unified pgsql_stmt_finish,
132+
* but maybe there still is an internal query that we omitted to adapt.
133+
* So instead of asserting let's just emit an informational notice,
134+
* and consume anyway (results consumption is handle-wise, so we have no formal
135+
* need for the statement). */
136+
if (first) {
137+
php_error_docref(NULL, E_NOTICE, "Internal error: unable to link a libpq result to consume, to its origin statement");
138+
first = false;
139+
}
140+
PQclear(pgsql_result);
141+
}
142+
}
143+
112144
static void _pdo_pgsql_notice(void *context, const char *message) /* {{{ */
113145
{
114146
pdo_dbh_t * dbh = (pdo_dbh_t *)context;
@@ -258,6 +290,10 @@ static void pgsql_handle_closer(pdo_dbh_t *dbh) /* {{{ */
258290
PQfinish(H->server);
259291
H->server = NULL;
260292
}
293+
if (H->cached_table_name) {
294+
efree(H->cached_table_name);
295+
H->cached_table_name = NULL;
296+
}
261297
if (H->einfo.errmsg) {
262298
pefree(H->einfo.errmsg, dbh->is_persistent);
263299
H->einfo.errmsg = NULL;
@@ -351,6 +387,7 @@ static zend_long pgsql_handle_doer(pdo_dbh_t *dbh, const zend_string *sql)
351387

352388
bool in_trans = pgsql_handle_in_transaction(dbh);
353389

390+
pgsql_finish_running_stmt(H);
354391
if (!(res = PQexec(H->server, ZSTR_VAL(sql)))) {
355392
/* fatal error */
356393
pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
@@ -426,6 +463,7 @@ static zend_string *pdo_pgsql_last_insert_id(pdo_dbh_t *dbh, const zend_string *
426463
PGresult *res;
427464
ExecStatusType status;
428465

466+
pgsql_finish_running_stmt(H);
429467
if (name == NULL) {
430468
res = PQexec(H->server, "SELECT LASTVAL()");
431469
} else {
@@ -589,6 +627,7 @@ static bool pdo_pgsql_transaction_cmd(const char *cmd, pdo_dbh_t *dbh)
589627
PGresult *res;
590628
bool ret = true;
591629

630+
pgsql_finish_running_stmt(H);
592631
res = PQexec(H->server, cmd);
593632

594633
if (PQresultStatus(res) != PGRES_COMMAND_OK) {
@@ -696,9 +735,8 @@ void pgsqlCopyFromArray_internal(INTERNAL_FUNCTION_PARAMETERS)
696735
/* Obtain db Handle */
697736
H = (pdo_pgsql_db_handle *)dbh->driver_data;
698737

699-
while ((pgsql_result = PQgetResult(H->server))) {
700-
PQclear(pgsql_result);
701-
}
738+
pgsql_discard_running_stmt(H);
739+
702740
pgsql_result = PQexec(H->server, query);
703741

704742
efree(query);
@@ -820,9 +858,8 @@ void pgsqlCopyFromFile_internal(INTERNAL_FUNCTION_PARAMETERS)
820858

821859
H = (pdo_pgsql_db_handle *)dbh->driver_data;
822860

823-
while ((pgsql_result = PQgetResult(H->server))) {
824-
PQclear(pgsql_result);
825-
}
861+
pgsql_discard_running_stmt(H);
862+
826863
pgsql_result = PQexec(H->server, query);
827864

828865
efree(query);
@@ -916,9 +953,7 @@ void pgsqlCopyToFile_internal(INTERNAL_FUNCTION_PARAMETERS)
916953
RETURN_FALSE;
917954
}
918955

919-
while ((pgsql_result = PQgetResult(H->server))) {
920-
PQclear(pgsql_result);
921-
}
956+
pgsql_discard_running_stmt(H);
922957

923958
/* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
924959
if (pg_fields) {
@@ -1007,9 +1042,7 @@ void pgsqlCopyToArray_internal(INTERNAL_FUNCTION_PARAMETERS)
10071042

10081043
H = (pdo_pgsql_db_handle *)dbh->driver_data;
10091044

1010-
while ((pgsql_result = PQgetResult(H->server))) {
1011-
PQclear(pgsql_result);
1012-
}
1045+
pgsql_discard_running_stmt(H);
10131046

10141047
/* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
10151048
if (pg_fields) {
@@ -1461,6 +1494,7 @@ static int pdo_pgsql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{
14611494

14621495
H->attached = 1;
14631496
H->pgoid = -1;
1497+
H->cached_table_oid = InvalidOid;
14641498

14651499
dbh->methods = &pgsql_methods;
14661500
dbh->alloc_own_columns = 1;

ext/pdo_pgsql/pgsql_statement.c

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@
5656
#define FLOAT8LABEL "float8"
5757
#define FLOAT8OID 701
5858

59-
#define FIN_DISCARD 0x1
60-
#define FIN_CLOSE 0x2
61-
#define FIN_ABORT 0x4
6259

6360

64-
65-
static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
61+
void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
6662
{
63+
if (!S) {
64+
return;
65+
}
66+
6767
pdo_pgsql_db_handle *H = S->H;
6868

6969
if (S->is_running_unbuffered && S->result && (fin_mode & FIN_ABORT)) {
@@ -113,9 +113,10 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
113113
}
114114

115115
S->is_prepared = false;
116-
if (H->running_stmt == S) {
117-
H->running_stmt = NULL;
118-
}
116+
}
117+
118+
if (H->running_stmt == S && (fin_mode & (FIN_CLOSE|FIN_ABORT))) {
119+
H->running_stmt = NULL;
119120
}
120121
}
121122

@@ -188,9 +189,8 @@ static int pgsql_stmt_execute(pdo_stmt_t *stmt)
188189
* and returns a PGRES_FATAL_ERROR when PQgetResult gets called for stmt 2 if DEALLOCATE
189190
* was called for stmt 1 inbetween
190191
* (maybe it will change with pipeline mode in libpq 14?) */
191-
if (S->is_unbuffered && H->running_stmt) {
192+
if (H->running_stmt && H->running_stmt->is_unbuffered) {
192193
pgsql_stmt_finish(H->running_stmt, FIN_CLOSE);
193-
H->running_stmt = NULL;
194194
}
195195
/* ensure that we free any previous unfetched results */
196196
pgsql_stmt_finish(S, 0);
@@ -702,12 +702,29 @@ static int pgsql_stmt_get_col(pdo_stmt_t *stmt, int colno, zval *result, enum pd
702702
return 1;
703703
}
704704

705-
static zend_always_inline char * pdo_pgsql_translate_oid_to_table(Oid oid, PGconn *conn)
705+
static zend_always_inline char * pdo_pgsql_translate_oid_to_table(Oid oid, pdo_pgsql_db_handle *H)
706706
{
707+
PGconn *conn = H->server;
707708
char *table_name = NULL;
708709
PGresult *tmp_res;
709710
char *querystr = NULL;
710711

712+
if (oid == H->cached_table_oid) {
713+
return H->cached_table_name;
714+
}
715+
716+
if (H->running_stmt && H->running_stmt->is_unbuffered) {
717+
/* in single-row mode, libpq forbids passing a new query
718+
* while we're still flushing the current one's result */
719+
return NULL;
720+
}
721+
722+
if (H->cached_table_name) {
723+
efree(H->cached_table_name);
724+
H->cached_table_name = NULL;
725+
H->cached_table_oid = InvalidOid;
726+
}
727+
711728
spprintf(&querystr, 0, "SELECT RELNAME FROM PG_CLASS WHERE OID=%d", oid);
712729

713730
if ((tmp_res = PQexec(conn, querystr)) == NULL || PQresultStatus(tmp_res) != PGRES_TUPLES_OK) {
@@ -724,6 +741,8 @@ static zend_always_inline char * pdo_pgsql_translate_oid_to_table(Oid oid, PGcon
724741
return 0;
725742
}
726743

744+
H->cached_table_oid = oid;
745+
H->cached_table_name = estrdup(table_name);
727746
table_name = estrdup(table_name);
728747

729748
PQclear(tmp_res);
@@ -752,10 +771,9 @@ static int pgsql_stmt_get_column_meta(pdo_stmt_t *stmt, zend_long colno, zval *r
752771

753772
table_oid = PQftable(S->result, colno);
754773
add_assoc_long(return_value, "pgsql:table_oid", table_oid);
755-
table_name = pdo_pgsql_translate_oid_to_table(table_oid, S->H->server);
774+
table_name = pdo_pgsql_translate_oid_to_table(table_oid, S->H);
756775
if (table_name) {
757-
add_assoc_string(return_value, "table", table_name);
758-
efree(table_name);
776+
add_assoc_string(return_value, "table", S->H->cached_table_name);
759777
}
760778

761779
switch (S->cols[colno].pgsql_type) {
@@ -794,6 +812,10 @@ static int pgsql_stmt_get_column_meta(pdo_stmt_t *stmt, zend_long colno, zval *r
794812
break;
795813
default:
796814
/* Fetch metadata from Postgres system catalogue */
815+
if (S->H->running_stmt && S->H->running_stmt->is_unbuffered) {
816+
/* libpq forbids calling a query while we're still reading the preceding one's */
817+
break;
818+
}
797819
spprintf(&q, 0, "SELECT TYPNAME FROM PG_TYPE WHERE OID=%u", S->cols[colno].pgsql_type);
798820
res = PQexec(S->H->server, q);
799821
efree(q);

ext/pdo_pgsql/php_pdo_pgsql_int.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ typedef struct {
4343
unsigned _reserved:31;
4444
pdo_pgsql_error_info einfo;
4545
Oid pgoid;
46+
Oid cached_table_oid;
47+
char *cached_table_name;
4648
unsigned int stmt_counter;
4749
bool emulate_prepares;
4850
bool disable_prepares;
@@ -90,6 +92,12 @@ extern int _pdo_pgsql_error(pdo_dbh_t *dbh, pdo_stmt_t *stmt, int errcode, const
9092

9193
extern const struct pdo_stmt_methods pgsql_stmt_methods;
9294

95+
#define FIN_DISCARD 0x1
96+
#define FIN_CLOSE 0x2
97+
#define FIN_ABORT 0x4
98+
99+
extern void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode);
100+
93101
#define pdo_pgsql_sqlstate(r) PQresultErrorField(r, PG_DIAG_SQLSTATE)
94102

95103
enum {

ext/pdo_pgsql/tests/gh15287.phpt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,17 @@ $res = []; while (($re = $stmt->fetch())) $res[] = $re; display($res);
132132
$stmt->execute([ 0 ]);
133133
$res = []; for ($i = -1; ++$i < 2;) $res[] = $stmt->fetch(); display($res);
134134
display($pdo->query("select * from t2")->fetchAll());
135+
136+
// Metadata calls the server for some operations (notably table oid-to-name conversion).
137+
// This will break libpq (that forbids a second PQexec before we consumed the first one).
138+
// Instead of either letting libpq return an error, or blindly forbid this call, we expect
139+
// being transparently provided at least attributes which do not require a server roundtrip.
140+
// And good news: column name is one of those "local" attributes.
141+
echo "=== meta ===\n";
142+
$stmt = $pdo->query("select * from t limit 2");
143+
echo "Starting with column " . $stmt->getColumnMeta(0)['name'] . ":\n";
144+
display($stmt->fetchAll());
145+
135146
?>
136147
--EXPECTF--
137148
=== non regression ===
@@ -181,3 +192,7 @@ multiple calls to the same prepared statement, some interrupted before having re
181192
0
182193
1
183194
678 ok
195+
=== meta ===
196+
Starting with column n:
197+
0 original
198+
1 non original

0 commit comments

Comments
 (0)