Skip to content

Commit ff41006

Browse files
ext/pdo_pgsql: Add Pdo\Pgsql::ATTR_CHUNK_SIZE for chunked result fetching
1 parent 853bf6f commit ff41006

14 files changed

Lines changed: 560 additions & 4 deletions

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ PHP NEWS
8686
connection busy for the next one. (KentarouTakeda)
8787
. Fixed a lazy fetch returning a row of NULLs after another statement
8888
took over the connection. (KentarouTakeda)
89+
. Added Pdo\Pgsql::ATTR_CHUNK_SIZE to fetch a result set in chunks of the
90+
given number of rows. (KentarouTakeda)
8991

9092
- Readline:
9193
. Fixed the interactive shell not waiting for the pager process to exit.

UPGRADING

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,11 @@ PHP 8.6 UPGRADE NOTES
389389
outcome is reported as 'accepted', 'rejected' or 'not_sent' in the
390390
early_data key of the crypto stream_get_meta_data() array.
391391

392+
- PDO_PGSQL:
393+
. Added Pdo\Pgsql::ATTR_CHUNK_SIZE, the number of rows a statement fetches
394+
per chunk. A value of 1 or more enters the lazy fetch mode of
395+
PDO::ATTR_PREFETCH => 0. Requires libpq 17 or later.
396+
392397
- Phar:
393398
. Overriding the getMTime() and getPathname() methods of SplFileInfo now
394399
influences the result of the phar buildFrom family of functions.

ext/pdo_pgsql/config.m4

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ if test "$PHP_PDO_PGSQL" != "no"; then
2525
or later).])],,
2626
[$PGSQL_LIBS])
2727

28+
old_CFLAGS=$CFLAGS
29+
CFLAGS="$CFLAGS $PGSQL_CFLAGS"
30+
31+
AC_CHECK_DECL([PGRES_TUPLES_CHUNK],
32+
PHP_CHECK_LIBRARY([pq], [PQsetChunkedRowsMode],
33+
[AC_DEFINE([HAVE_PG_SET_CHUNKED_ROWS_SIZE], [1],
34+
[Define to 1 if libpq has the 'PQsetChunkedRowsMode' function (PostgreSQL
35+
17 or later).])],,
36+
[$PGSQL_LIBS]),,
37+
[#include <libpq-fe.h>]
38+
)
39+
40+
CFLAGS=$old_CFLAGS
41+
2842
PHP_CHECK_PDO_INCLUDES
2943

3044
PHP_NEW_EXTENSION([pdo_pgsql],

ext/pdo_pgsql/pdo_pgsql.stub.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ class Pgsql extends \PDO
1818
public const int ATTR_RESULT_MEMORY_SIZE = UNKNOWN;
1919
#endif
2020

21+
#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
22+
/** @cvalue PDO_PGSQL_ATTR_CHUNK_SIZE */
23+
public const int ATTR_CHUNK_SIZE = UNKNOWN;
24+
#endif
25+
2126
/** @cvalue PGSQL_TRANSACTION_IDLE */
2227
#[\Deprecated(since: "8.5", message: "as it has no effect")]
2328
public const int TRANSACTION_IDLE = UNKNOWN;

ext/pdo_pgsql/pdo_pgsql_arginfo.h

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/pdo_pgsql/pgsql_driver.c

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,18 @@ static void pgsql_handle_closer(pdo_dbh_t *dbh) /* {{{ */
266266
}
267267
/* }}} */
268268

269+
#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
270+
static bool pdo_pgsql_check_chunk_size(zend_long size)
271+
{
272+
if (size < 0 || ZEND_LONG_EXCEEDS_INT(size)) {
273+
zend_value_error("Pdo\\Pgsql::ATTR_CHUNK_SIZE must be between 0 and %d", INT_MAX);
274+
return false;
275+
}
276+
277+
return true;
278+
}
279+
#endif
280+
269281
static bool pgsql_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *stmt, zval *driver_options)
270282
{
271283
pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
@@ -318,6 +330,38 @@ static bool pgsql_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *
318330
: H->default_fetching_laziness
319331
;
320332

333+
#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
334+
bool chunk_size_given = driver_options
335+
&& (val = zend_hash_index_find(Z_ARRVAL_P(driver_options), PDO_PGSQL_ATTR_CHUNK_SIZE));
336+
337+
if (chunk_size_given) {
338+
if (!pdo_get_long_param(&lval, val)) {
339+
return false;
340+
}
341+
S->chunk_size = lval;
342+
} else {
343+
S->chunk_size = H->default_chunk_size;
344+
}
345+
346+
if (!pdo_pgsql_check_chunk_size(S->chunk_size)) {
347+
return false;
348+
}
349+
350+
if (S->chunk_size >= 1 && scrollable) {
351+
if (chunk_size_given) {
352+
zend_value_error("Pdo\\Pgsql::ATTR_CHUNK_SIZE cannot be combined with "
353+
"PDO::ATTR_CURSOR set to PDO::CURSOR_SCROLL");
354+
return false;
355+
}
356+
357+
S->chunk_size = 0;
358+
}
359+
360+
if (S->chunk_size >= 1) {
361+
S->is_unbuffered = true;
362+
}
363+
#endif
364+
321365
ret = pdo_parse_params(stmt, sql, &nsql);
322366

323367
if (ret == -1) {
@@ -473,6 +517,12 @@ static int pdo_pgsql_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return_
473517
ZVAL_BOOL(return_value, H->disable_prepares);
474518
break;
475519

520+
#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
521+
case PDO_PGSQL_ATTR_CHUNK_SIZE:
522+
ZVAL_LONG(return_value, H->default_chunk_size);
523+
break;
524+
#endif
525+
476526
case PDO_ATTR_CLIENT_VERSION: {
477527
char buf[16];
478528
pdo_libpq_version(buf, sizeof(buf));
@@ -1377,6 +1427,20 @@ static bool pdo_pgsql_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val)
13771427
}
13781428
H->default_fetching_laziness = !bval;
13791429
return true;
1430+
#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
1431+
case PDO_PGSQL_ATTR_CHUNK_SIZE: {
1432+
zend_long lval;
1433+
1434+
if (!pdo_get_long_param(&lval, val)) {
1435+
return false;
1436+
}
1437+
if (!pdo_pgsql_check_chunk_size(lval)) {
1438+
return false;
1439+
}
1440+
H->default_chunk_size = lval;
1441+
return true;
1442+
}
1443+
#endif
13801444
default:
13811445
return false;
13821446
}

ext/pdo_pgsql/pgsql_statement.c

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@
5858
#define FIN_CLOSE 0x2
5959
#define FIN_ABORT 0x4
6060

61+
static bool pgsql_result_status_ok(ExecStatusType status)
62+
{
63+
switch (status) {
64+
case PGRES_COMMAND_OK:
65+
case PGRES_TUPLES_OK:
66+
case PGRES_SINGLE_TUPLE:
67+
#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
68+
case PGRES_TUPLES_CHUNK:
69+
#endif
70+
return true;
71+
default:
72+
return false;
73+
}
74+
}
75+
6176

6277

6378
static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
@@ -354,16 +369,24 @@ static int pgsql_stmt_execute(pdo_stmt_t *stmt)
354369
return 0;
355370
}
356371
S->is_running_unbuffered = true;
372+
/* no matter if they return 0: PQ then transparently fallbacks to full result fetching */
373+
#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
374+
if (S->chunk_size >= 1) {
375+
(void)PQsetChunkedRowsMode(H->server, (int)S->chunk_size);
376+
} else {
377+
(void)PQsetSingleRowMode(H->server);
378+
}
379+
#else
357380
(void)PQsetSingleRowMode(H->server);
358-
/* no matter if it returns 0: PQ then transparently fallbacks to full result fetching */
381+
#endif
359382

360383
/* try a first fetch to at least have column names and so on */
361384
S->result = PQgetResult(S->H->server);
362385
}
363386

364387
status = PQresultStatus(S->result);
365388

366-
if (status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK && status != PGRES_SINGLE_TUPLE) {
389+
if (!pgsql_result_status_ok(status)) {
367390
pdo_pgsql_error_stmt(stmt, status, pdo_pgsql_sqlstate(S->result));
368391
return 0;
369392
}
@@ -607,7 +630,7 @@ static int pgsql_stmt_fetch(pdo_stmt_t *stmt,
607630
}
608631
status = PQresultStatus(S->result);
609632

610-
if (status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK && status != PGRES_SINGLE_TUPLE) {
633+
if (!pgsql_result_status_ok(status)) {
611634
pdo_pgsql_error_stmt(stmt, status, pdo_pgsql_sqlstate(S->result));
612635
return 0;
613636
}
@@ -884,6 +907,12 @@ static int pgsql_stmt_get_attr(pdo_stmt_t *stmt, zend_long attr, zval *val)
884907
return 1;
885908
#endif
886909

910+
#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
911+
case PDO_PGSQL_ATTR_CHUNK_SIZE:
912+
ZVAL_LONG(val, S->chunk_size);
913+
return 1;
914+
#endif
915+
887916
default:
888917
(void)S;
889918
return 0;

ext/pdo_pgsql/php_pdo_pgsql_int.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ typedef struct {
4747
HashTable *lob_streams;
4848
zend_fcall_info_cache *notice_callback;
4949
bool default_fetching_laziness;
50+
zend_long default_chunk_size;
5051
pdo_pgsql_stmt *running_stmt;
5152
} pdo_pgsql_db_handle;
5253

@@ -66,6 +67,7 @@ struct pdo_pgsql_stmt {
6667
int *param_formats;
6768
Oid *param_types;
6869
int current_row;
70+
zend_long chunk_size;
6971
bool is_prepared;
7072
bool is_unbuffered;
7173
bool is_running_unbuffered;
@@ -93,6 +95,7 @@ extern const struct pdo_stmt_methods pgsql_stmt_methods;
9395
enum {
9496
PDO_PGSQL_ATTR_DISABLE_PREPARES = PDO_ATTR_DRIVER_SPECIFIC,
9597
PDO_PGSQL_ATTR_RESULT_MEMORY_SIZE,
98+
PDO_PGSQL_ATTR_CHUNK_SIZE,
9699
};
97100

98101
struct pdo_pgsql_lob_self {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
--TEST--
2+
PDO PgSQL Pdo\Pgsql::ATTR_CHUNK_SIZE splits a result set into chunks of rows
3+
--EXTENSIONS--
4+
pdo_pgsql
5+
--SKIPIF--
6+
<?php
7+
require __DIR__ . '/config.inc';
8+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
9+
PDOTest::skip();
10+
if (!defined('Pdo\Pgsql::ATTR_CHUNK_SIZE')) die('skip libpq >= 17 required');
11+
?>
12+
--FILE--
13+
<?php
14+
15+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
16+
$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
17+
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
18+
19+
// rowCount() reports the size of the chunk being consumed while fetching unbuffered
20+
function run(PDO $pdo, string $label, array $options): void
21+
{
22+
$stmt = $pdo->prepare("SELECT * FROM generate_series(1, 10)", $options);
23+
$stmt->execute();
24+
25+
$values = [];
26+
$sizes = [];
27+
28+
while (($row = $stmt->fetch(PDO::FETCH_NUM))) {
29+
$values[] = $row[0];
30+
$sizes[] = $stmt->rowCount();
31+
}
32+
33+
printf("%s\n values=%s\n chunk sizes=%s\n",
34+
$label, implode(',', $values), implode(',', $sizes));
35+
}
36+
37+
run($pdo, 'buffered (default)', []);
38+
run($pdo, 'ATTR_PREFETCH => 0', [PDO::ATTR_PREFETCH => 0]);
39+
run($pdo, 'ATTR_CHUNK_SIZE => 1', [Pdo\Pgsql::ATTR_CHUNK_SIZE => 1]);
40+
run($pdo, 'ATTR_CHUNK_SIZE => 4 (last chunk is partial)', [Pdo\Pgsql::ATTR_CHUNK_SIZE => 4]);
41+
run($pdo, 'ATTR_CHUNK_SIZE => 5 (divides evenly)', [Pdo\Pgsql::ATTR_CHUNK_SIZE => 5]);
42+
run($pdo, 'ATTR_CHUNK_SIZE => 99 (larger than the result)', [Pdo\Pgsql::ATTR_CHUNK_SIZE => 99]);
43+
44+
?>
45+
--EXPECT--
46+
buffered (default)
47+
values=1,2,3,4,5,6,7,8,9,10
48+
chunk sizes=10,10,10,10,10,10,10,10,10,10
49+
ATTR_PREFETCH => 0
50+
values=1,2,3,4,5,6,7,8,9,10
51+
chunk sizes=1,1,1,1,1,1,1,1,1,1
52+
ATTR_CHUNK_SIZE => 1
53+
values=1,2,3,4,5,6,7,8,9,10
54+
chunk sizes=1,1,1,1,1,1,1,1,1,1
55+
ATTR_CHUNK_SIZE => 4 (last chunk is partial)
56+
values=1,2,3,4,5,6,7,8,9,10
57+
chunk sizes=4,4,4,4,4,4,4,4,2,2
58+
ATTR_CHUNK_SIZE => 5 (divides evenly)
59+
values=1,2,3,4,5,6,7,8,9,10
60+
chunk sizes=5,5,5,5,5,5,5,5,5,5
61+
ATTR_CHUNK_SIZE => 99 (larger than the result)
62+
values=1,2,3,4,5,6,7,8,9,10
63+
chunk sizes=10,10,10,10,10,10,10,10,10,10

0 commit comments

Comments
 (0)