Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ PHP NEWS
statement with emulated or disabled prepares is destroyed, a connection
left busy for the next fetch, and rows delivered from a result another
statement took over. (KentarouTakeda)
. Added Pdo\Pgsql::ATTR_CHUNK_SIZE to fetch a result set in chunks of the
given number of rows. (KentarouTakeda)

- PGSQL:
. Fixed the class name casing of pg_close_stmt()'s connection parameter.
Expand Down
7 changes: 7 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,13 @@ PHP 8.6 UPGRADE NOTES
outcome is reported as 'accepted', 'rejected' or 'not_sent' in the
early_data key of the crypto stream_get_meta_data() array.

- PDO_PGSQL:
. Added Pdo\Pgsql::ATTR_CHUNK_SIZE, the number of rows a statement fetches
per chunk. A value of 1 or more enters the lazy fetch mode of
PDO::ATTR_PREFETCH => 0. Statements that are prepared with neither it nor
PDO::ATTR_PREFETCH fall back to the value set on the connection.
Requires libpq 17 or later.

- Phar:
. Overriding the getMTime() and getPathname() methods of SplFileInfo now
influences the result of the phar buildFrom family of functions.
Expand Down
14 changes: 14 additions & 0 deletions ext/pdo_pgsql/config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ if test "$PHP_PDO_PGSQL" != "no"; then
or later).])],,
[$PGSQL_LIBS])

old_CFLAGS=$CFLAGS
CFLAGS="$CFLAGS $PGSQL_CFLAGS"

AC_CHECK_DECL([PGRES_TUPLES_CHUNK],
PHP_CHECK_LIBRARY([pq], [PQsetChunkedRowsMode],
[AC_DEFINE([HAVE_PG_SET_CHUNKED_ROWS_SIZE], [1],
[Define to 1 if libpq has the 'PQsetChunkedRowsMode' function (PostgreSQL
17 or later).])],,
[$PGSQL_LIBS]),,
[#include <libpq-fe.h>]
)

CFLAGS=$old_CFLAGS

PHP_CHECK_PDO_INCLUDES

PHP_NEW_EXTENSION([pdo_pgsql],
Expand Down
5 changes: 5 additions & 0 deletions ext/pdo_pgsql/pdo_pgsql.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ class Pgsql extends \PDO
public const int ATTR_RESULT_MEMORY_SIZE = UNKNOWN;
#endif

#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
/** @cvalue PDO_PGSQL_ATTR_CHUNK_SIZE */
public const int ATTR_CHUNK_SIZE = UNKNOWN;
#endif

/** @cvalue PGSQL_TRANSACTION_IDLE */
#[\Deprecated(since: "8.5", message: "as it has no effect")]
public const int TRANSACTION_IDLE = UNKNOWN;
Expand Down
10 changes: 9 additions & 1 deletion ext/pdo_pgsql/pdo_pgsql_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

82 changes: 74 additions & 8 deletions ext/pdo_pgsql/pgsql_driver.c
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,18 @@ static void pgsql_handle_closer(pdo_dbh_t *dbh) /* {{{ */
}
/* }}} */

#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
static bool pdo_pgsql_check_chunk_size(zend_long size)
{
if (size < 0 || ZEND_LONG_EXCEEDS_INT(size)) {
zend_value_error("Pdo\\Pgsql::ATTR_CHUNK_SIZE must be between 0 and %d", INT_MAX);
return false;
}

return true;
}
#endif

static bool pgsql_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *stmt, zval *driver_options)
{
pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
Expand All @@ -285,6 +297,48 @@ static bool pgsql_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *
scrollable = pdo_attr_lval(driver_options, PDO_ATTR_CURSOR,
PDO_CURSOR_FWDONLY) == PDO_CURSOR_SCROLL;

bool prefetch_given = driver_options
&& (val = zend_hash_index_find(Z_ARRVAL_P(driver_options), PDO_ATTR_PREFETCH));

S->is_unbuffered = prefetch_given && pdo_get_long_param(&lval, val)
? !lval
: H->default_fetching_laziness;

#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
bool chunk_size_given = driver_options
&& (val = zend_hash_index_find(Z_ARRVAL_P(driver_options), PDO_PGSQL_ATTR_CHUNK_SIZE));

if (chunk_size_given) {
if (!pdo_get_long_param(&lval, val)) {
return false;
}
S->chunk_size = lval;
} else if (prefetch_given) {
/* the statement's own prefetch replaces an inherited chunk size */
S->chunk_size = 0;
} else {
S->chunk_size = H->default_chunk_size;
}

if (!pdo_pgsql_check_chunk_size(S->chunk_size)) {
return false;
}

if (S->chunk_size >= 1 && scrollable) {
if (chunk_size_given) {
zend_value_error("Pdo\\Pgsql::ATTR_CHUNK_SIZE cannot be combined with "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: there is a leak with S->cursor_name

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6bc7647. I moved the new code and the values it depends on above the S->cursor_name allocation. Thanks.

"PDO::ATTR_CURSOR set to PDO::CURSOR_SCROLL");
return false;
}

S->chunk_size = 0;
}

if (S->chunk_size >= 1) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that kind of bother me but do not know why yet, I ll have a better look this week end

S->is_unbuffered = true;
}
#endif

if (scrollable) {
if (S->cursor_name) {
efree(S->cursor_name);
Expand All @@ -310,14 +364,6 @@ static bool pgsql_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *
stmt->named_rewrite_template = "$%d";
}

S->is_unbuffered =
driver_options
&& (val = zend_hash_index_find(Z_ARRVAL_P(driver_options), PDO_ATTR_PREFETCH))
&& pdo_get_long_param(&lval, val)
? !lval
: H->default_fetching_laziness
;

ret = pdo_parse_params(stmt, sql, &nsql);

if (ret == -1) {
Expand Down Expand Up @@ -473,6 +519,12 @@ static int pdo_pgsql_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return_
ZVAL_BOOL(return_value, H->disable_prepares);
break;

#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
case PDO_PGSQL_ATTR_CHUNK_SIZE:
ZVAL_LONG(return_value, H->default_chunk_size);
break;
#endif

case PDO_ATTR_CLIENT_VERSION: {
char buf[16];
pdo_libpq_version(buf, sizeof(buf));
Expand Down Expand Up @@ -1377,6 +1429,20 @@ static bool pdo_pgsql_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val)
}
H->default_fetching_laziness = !bval;
return true;
#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
case PDO_PGSQL_ATTR_CHUNK_SIZE: {
zend_long lval;

if (!pdo_get_long_param(&lval, val)) {
return false;
}
if (!pdo_pgsql_check_chunk_size(lval)) {
return false;
}
H->default_chunk_size = lval;
return true;
}
#endif
default:
return false;
}
Expand Down
35 changes: 32 additions & 3 deletions ext/pdo_pgsql/pgsql_statement.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@
#define FIN_CLOSE 0x2
#define FIN_ABORT 0x4

static bool pgsql_result_status_ok(ExecStatusType status)
{
switch (status) {
case PGRES_COMMAND_OK:
case PGRES_TUPLES_OK:
case PGRES_SINGLE_TUPLE:
#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
case PGRES_TUPLES_CHUNK:
#endif
return true;
default:
return false;
}
}



static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
Expand Down Expand Up @@ -354,16 +369,24 @@ static int pgsql_stmt_execute(pdo_stmt_t *stmt)
return 0;
}
S->is_running_unbuffered = true;
/* no matter if they return 0: PQ then transparently fallbacks to full result fetching */
#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
if (S->chunk_size >= 1) {
(void)PQsetChunkedRowsMode(H->server, (int)S->chunk_size);
} else {
(void)PQsetSingleRowMode(H->server);
}
#else
(void)PQsetSingleRowMode(H->server);
/* no matter if it returns 0: PQ then transparently fallbacks to full result fetching */
#endif

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

status = PQresultStatus(S->result);

if (status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK && status != PGRES_SINGLE_TUPLE) {
if (!pgsql_result_status_ok(status)) {
pdo_pgsql_error_stmt(stmt, status, pdo_pgsql_sqlstate(S->result));
return 0;
}
Expand Down Expand Up @@ -607,7 +630,7 @@ static int pgsql_stmt_fetch(pdo_stmt_t *stmt,
}
status = PQresultStatus(S->result);

if (status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK && status != PGRES_SINGLE_TUPLE) {
if (!pgsql_result_status_ok(status)) {
pdo_pgsql_error_stmt(stmt, status, pdo_pgsql_sqlstate(S->result));
return 0;
}
Expand Down Expand Up @@ -884,6 +907,12 @@ static int pgsql_stmt_get_attr(pdo_stmt_t *stmt, zend_long attr, zval *val)
return 1;
#endif

#ifdef HAVE_PG_SET_CHUNKED_ROWS_SIZE
case PDO_PGSQL_ATTR_CHUNK_SIZE:
ZVAL_LONG(val, S->chunk_size);
return 1;
#endif

default:
(void)S;
return 0;
Expand Down
3 changes: 3 additions & 0 deletions ext/pdo_pgsql/php_pdo_pgsql_int.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ typedef struct {
HashTable *lob_streams;
zend_fcall_info_cache *notice_callback;
bool default_fetching_laziness;
zend_long default_chunk_size;
pdo_pgsql_stmt *running_stmt;
} pdo_pgsql_db_handle;

Expand All @@ -66,6 +67,7 @@ struct pdo_pgsql_stmt {
int *param_formats;
Oid *param_types;
int current_row;
zend_long chunk_size;
bool is_prepared;
bool is_unbuffered;
bool is_running_unbuffered;
Expand Down Expand Up @@ -93,6 +95,7 @@ extern const struct pdo_stmt_methods pgsql_stmt_methods;
enum {
PDO_PGSQL_ATTR_DISABLE_PREPARES = PDO_ATTR_DRIVER_SPECIFIC,
PDO_PGSQL_ATTR_RESULT_MEMORY_SIZE,
PDO_PGSQL_ATTR_CHUNK_SIZE,
};

struct pdo_pgsql_lob_self {
Expand Down
63 changes: 63 additions & 0 deletions ext/pdo_pgsql/tests/chunk_size.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
--TEST--
PDO PgSQL Pdo\Pgsql::ATTR_CHUNK_SIZE splits a result set into chunks of rows
--EXTENSIONS--
pdo_pgsql
--SKIPIF--
<?php
require __DIR__ . '/config.inc';
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
PDOTest::skip();
if (!defined('Pdo\Pgsql::ATTR_CHUNK_SIZE')) die('skip libpq >= 17 required');
?>
--FILE--
<?php

require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// rowCount() reports the size of the chunk being consumed while fetching unbuffered
function run(PDO $pdo, string $label, array $options): void
{
$stmt = $pdo->prepare("SELECT * FROM generate_series(1, 10)", $options);
$stmt->execute();

$values = [];
$sizes = [];

while (($row = $stmt->fetch(PDO::FETCH_NUM))) {
$values[] = $row[0];
$sizes[] = $stmt->rowCount();
}

printf("%s\n values=%s\n chunk sizes=%s\n",
$label, implode(',', $values), implode(',', $sizes));
}

run($pdo, 'buffered (default)', []);
run($pdo, 'ATTR_PREFETCH => 0', [PDO::ATTR_PREFETCH => 0]);
run($pdo, 'ATTR_CHUNK_SIZE => 1', [Pdo\Pgsql::ATTR_CHUNK_SIZE => 1]);
run($pdo, 'ATTR_CHUNK_SIZE => 4 (last chunk is partial)', [Pdo\Pgsql::ATTR_CHUNK_SIZE => 4]);
run($pdo, 'ATTR_CHUNK_SIZE => 5 (divides evenly)', [Pdo\Pgsql::ATTR_CHUNK_SIZE => 5]);
run($pdo, 'ATTR_CHUNK_SIZE => 99 (larger than the result)', [Pdo\Pgsql::ATTR_CHUNK_SIZE => 99]);

?>
--EXPECT--
buffered (default)
values=1,2,3,4,5,6,7,8,9,10
chunk sizes=10,10,10,10,10,10,10,10,10,10
ATTR_PREFETCH => 0
values=1,2,3,4,5,6,7,8,9,10
chunk sizes=1,1,1,1,1,1,1,1,1,1
ATTR_CHUNK_SIZE => 1
values=1,2,3,4,5,6,7,8,9,10
chunk sizes=1,1,1,1,1,1,1,1,1,1
ATTR_CHUNK_SIZE => 4 (last chunk is partial)
values=1,2,3,4,5,6,7,8,9,10
chunk sizes=4,4,4,4,4,4,4,4,2,2
ATTR_CHUNK_SIZE => 5 (divides evenly)
values=1,2,3,4,5,6,7,8,9,10
chunk sizes=5,5,5,5,5,5,5,5,5,5
ATTR_CHUNK_SIZE => 99 (larger than the result)
values=1,2,3,4,5,6,7,8,9,10
chunk sizes=10,10,10,10,10,10,10,10,10,10
Loading
Loading