ext/pdo_pgsql: adding pgsqlSetNoticeCallback

Allows a callback to be triggered on every notice sent by PostgreSQL.

Such notices can be sent with a RAISE NOTICE in PL/pgSQL; in a long running
stored procedure, they prove useful as realtime checkpoint indicators.

close GH-6764
This commit is contained in:
Guillaume Outters 2021-03-12 00:16:18 +01:00 committed by David Carlier
parent 182fee1447
commit c265b9085a
No known key found for this signature in database
GPG Key ID: CEF290BB40D2086B
10 changed files with 238 additions and 4 deletions

2
NEWS
View File

@ -178,6 +178,8 @@ PHP NEWS
. Fixed native float support with pdo_pgsql query results. (Yurunsoft) . Fixed native float support with pdo_pgsql query results. (Yurunsoft)
. Added class PdoPgsql. (danack, kocsismate) . Added class PdoPgsql. (danack, kocsismate)
. Retrieve the memory usage of the query result resource. (KentarouTakeda) . Retrieve the memory usage of the query result resource. (KentarouTakeda)
. Added PDO::pgsqlSetNoticeCallBack method to receive DB notices.
(outtersg)
- PDO_SQLITE: - PDO_SQLITE:
. Added class PdoSqlite. (danack, kocsismate) . Added class PdoSqlite. (danack, kocsismate)

View File

@ -522,6 +522,10 @@ PHP 8.4 UPGRADE NOTES
. Added pcntl_getqos_class to get the QoS level (aka performance and related . Added pcntl_getqos_class to get the QoS level (aka performance and related
energy consumption) of the current process and pcntl_setqos_class to set it. energy consumption) of the current process and pcntl_setqos_class to set it.
- PDO_PGSQL:
. Added PDO::pgsqlSetNoticeCallback to allow a callback to be triggered on
every notice sent (e.g. RAISE NOTICE).
- PGSQL: - PGSQL:
. Added pg_change_password to alter a given user's password. It handles . Added pg_change_password to alter a given user's password. It handles
transparently the password encryption from the database settings. transparently the password encryption from the database settings.

View File

@ -102,9 +102,16 @@ int _pdo_pgsql_error(pdo_dbh_t *dbh, pdo_stmt_t *stmt, int errcode, const char *
} }
/* }}} */ /* }}} */
static void _pdo_pgsql_notice(pdo_dbh_t *dbh, const char *message) /* {{{ */ static void _pdo_pgsql_notice(void *context, const char *message) /* {{{ */
{ {
/* pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; */ pdo_dbh_t * dbh = (pdo_dbh_t *)context;
zend_fcall_info_cache *fc = ((pdo_pgsql_db_handle *)dbh->driver_data)->notice_callback;
if (fc) {
zval zarg;
ZVAL_STRING(&zarg, message);
zend_call_known_fcc(fc, NULL, 1, &zarg, NULL);
zval_ptr_dtor_str(&zarg);
}
} }
/* }}} */ /* }}} */
@ -125,6 +132,16 @@ static void pdo_pgsql_fetch_error_func(pdo_dbh_t *dbh, pdo_stmt_t *stmt, zval *i
} }
/* }}} */ /* }}} */
static void pdo_pgsql_cleanup_notice_callback(pdo_pgsql_db_handle *H) /* {{{ */
{
if (H->notice_callback) {
zend_fcc_dtor(H->notice_callback);
efree(H->notice_callback);
H->notice_callback = NULL;
}
}
/* }}} */
/* {{{ pdo_pgsql_create_lob_stream */ /* {{{ pdo_pgsql_create_lob_stream */
static ssize_t pgsql_lob_write(php_stream *stream, const char *buf, size_t count) static ssize_t pgsql_lob_write(php_stream *stream, const char *buf, size_t count)
{ {
@ -229,6 +246,7 @@ static void pgsql_handle_closer(pdo_dbh_t *dbh) /* {{{ */
pefree(H->lob_streams, dbh->is_persistent); pefree(H->lob_streams, dbh->is_persistent);
H->lob_streams = NULL; H->lob_streams = NULL;
} }
pdo_pgsql_cleanup_notice_callback(H);
if (H->server) { if (H->server) {
PQfinish(H->server); PQfinish(H->server);
H->server = NULL; H->server = NULL;
@ -1224,6 +1242,30 @@ PHP_METHOD(PDO_PGSql_Ext, pgsqlGetPid)
} }
/* }}} */ /* }}} */
/* {{{ proto void PDO::pgsqlSetNoticeCallback(mixed callback)
Sets a callback to receive DB notices (after client_min_messages has been set) */
PHP_METHOD(PDO_PGSql_Ext, pgsqlSetNoticeCallback)
{
zend_fcall_info fci = empty_fcall_info;
zend_fcall_info_cache fcc = empty_fcall_info_cache;
if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "F!", &fci, &fcc)) {
RETURN_THROWS();
}
pdo_dbh_t *dbh = Z_PDO_DBH_P(ZEND_THIS);
PDO_CONSTRUCT_CHECK;
pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
pdo_pgsql_cleanup_notice_callback(H);
if (ZEND_FCC_INITIALIZED(fcc)) {
H->notice_callback = emalloc(sizeof(zend_fcall_info_cache));
zend_fcc_dup(H->notice_callback, &fcc);
}
}
/* }}} */
static const zend_function_entry *pdo_pgsql_get_driver_methods(pdo_dbh_t *dbh, int kind) static const zend_function_entry *pdo_pgsql_get_driver_methods(pdo_dbh_t *dbh, int kind)
{ {
switch (kind) { switch (kind) {
@ -1341,7 +1383,7 @@ static int pdo_pgsql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{
goto cleanup; goto cleanup;
} }
PQsetNoticeProcessor(H->server, (void(*)(void*,const char*))_pdo_pgsql_notice, (void *)&dbh); PQsetNoticeProcessor(H->server, _pdo_pgsql_notice, (void *)dbh);
H->attached = 1; H->attached = 1;
H->pgoid = -1; H->pgoid = -1;

View File

@ -33,4 +33,7 @@ class PDO_PGSql_Ext {
/** @tentative-return-type */ /** @tentative-return-type */
public function pgsqlGetPid(): int {} public function pgsqlGetPid(): int {}
/** @tentative-return-type */
public function pgsqlSetNoticeCallback(?callable $callback): void {}
} }

View File

@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead. /* This is a generated file, edit the .stub.php file instead.
* Stub hash: 9bb79af98dbb7c171fd9533aeabece4937a06cd2 */ * Stub hash: 14174ab18f198b9916f83986d10c93b657d8ffb9 */
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_PDO_PGSql_Ext_pgsqlCopyFromArray, 0, 2, _IS_BOOL, 0) ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_PDO_PGSql_Ext_pgsqlCopyFromArray, 0, 2, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, tableName, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, tableName, IS_STRING, 0)
@ -46,6 +46,10 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_PDO_PGSql_Ext_pgsqlGetPid, 0, 0, IS_LONG, 0) ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_PDO_PGSql_Ext_pgsqlGetPid, 0, 0, IS_LONG, 0)
ZEND_END_ARG_INFO() ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_PDO_PGSql_Ext_pgsqlSetNoticeCallback, 0, 1, IS_VOID, 0)
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 1)
ZEND_END_ARG_INFO()
ZEND_METHOD(PDO_PGSql_Ext, pgsqlCopyFromArray); ZEND_METHOD(PDO_PGSql_Ext, pgsqlCopyFromArray);
ZEND_METHOD(PDO_PGSql_Ext, pgsqlCopyFromFile); ZEND_METHOD(PDO_PGSql_Ext, pgsqlCopyFromFile);
ZEND_METHOD(PDO_PGSql_Ext, pgsqlCopyToArray); ZEND_METHOD(PDO_PGSql_Ext, pgsqlCopyToArray);
@ -55,6 +59,7 @@ ZEND_METHOD(PDO_PGSql_Ext, pgsqlLOBOpen);
ZEND_METHOD(PDO_PGSql_Ext, pgsqlLOBUnlink); ZEND_METHOD(PDO_PGSql_Ext, pgsqlLOBUnlink);
ZEND_METHOD(PDO_PGSql_Ext, pgsqlGetNotify); ZEND_METHOD(PDO_PGSql_Ext, pgsqlGetNotify);
ZEND_METHOD(PDO_PGSql_Ext, pgsqlGetPid); ZEND_METHOD(PDO_PGSql_Ext, pgsqlGetPid);
ZEND_METHOD(PDO_PGSql_Ext, pgsqlSetNoticeCallback);
static const zend_function_entry class_PDO_PGSql_Ext_methods[] = { static const zend_function_entry class_PDO_PGSql_Ext_methods[] = {
ZEND_ME(PDO_PGSql_Ext, pgsqlCopyFromArray, arginfo_class_PDO_PGSql_Ext_pgsqlCopyFromArray, ZEND_ACC_PUBLIC) ZEND_ME(PDO_PGSql_Ext, pgsqlCopyFromArray, arginfo_class_PDO_PGSql_Ext_pgsqlCopyFromArray, ZEND_ACC_PUBLIC)
@ -66,5 +71,6 @@ static const zend_function_entry class_PDO_PGSql_Ext_methods[] = {
ZEND_ME(PDO_PGSql_Ext, pgsqlLOBUnlink, arginfo_class_PDO_PGSql_Ext_pgsqlLOBUnlink, ZEND_ACC_PUBLIC) ZEND_ME(PDO_PGSql_Ext, pgsqlLOBUnlink, arginfo_class_PDO_PGSql_Ext_pgsqlLOBUnlink, ZEND_ACC_PUBLIC)
ZEND_ME(PDO_PGSql_Ext, pgsqlGetNotify, arginfo_class_PDO_PGSql_Ext_pgsqlGetNotify, ZEND_ACC_PUBLIC) ZEND_ME(PDO_PGSql_Ext, pgsqlGetNotify, arginfo_class_PDO_PGSql_Ext_pgsqlGetNotify, ZEND_ACC_PUBLIC)
ZEND_ME(PDO_PGSql_Ext, pgsqlGetPid, arginfo_class_PDO_PGSql_Ext_pgsqlGetPid, ZEND_ACC_PUBLIC) ZEND_ME(PDO_PGSql_Ext, pgsqlGetPid, arginfo_class_PDO_PGSql_Ext_pgsqlGetPid, ZEND_ACC_PUBLIC)
ZEND_ME(PDO_PGSql_Ext, pgsqlSetNoticeCallback, arginfo_class_PDO_PGSql_Ext_pgsqlSetNoticeCallback, ZEND_ACC_PUBLIC)
ZEND_FE_END ZEND_FE_END
}; };

View File

@ -46,6 +46,7 @@ typedef struct {
bool disable_native_prepares; /* deprecated since 5.6 */ bool disable_native_prepares; /* deprecated since 5.6 */
bool disable_prepares; bool disable_prepares;
HashTable *lob_streams; HashTable *lob_streams;
zend_fcall_info_cache *notice_callback;
} pdo_pgsql_db_handle; } pdo_pgsql_db_handle;
typedef struct { typedef struct {

View File

@ -0,0 +1,23 @@
<?php
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
require_once dirname(__FILE__) . '/config.inc';
$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
attach($db);
$db->beginTransaction();
$db->exec("set client_min_messages to notice");
$db->exec("create temporary table t (a varchar(3))");
$db->exec("create function hey() returns trigger as \$\$ begin new.a := 'oh'; raise notice 'I tampered your data, did you know?'; return new; end; \$\$ language plpgsql");
$db->exec("create trigger hop before insert on t for each row execute procedure hey()");
$db->exec("insert into t values ('ah')");
attach($db, 'Re');
$db->exec("delete from t");
$db->exec("insert into t values ('ah')");
$db->pgsqlSetNoticeCallback(null);
$db->exec("delete from t");
$db->exec("insert into t values ('ah')");
var_dump($db->query("select * from t")->fetchAll(PDO::FETCH_ASSOC));
echo "Done\n";
$db->rollback();
?>

View File

@ -0,0 +1,30 @@
--TEST--
pgsqlSetNoticeCallback catches Postgres "raise notice".
--SKIPIF--
<?php
if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
require_once dirname(__FILE__) . '/config.inc';
PDOTest::skip();
?>
--FILE--
<?php
function disp($message) { echo trim($message)."\n"; }
function dispRe($message) { echo "Re".trim($message)."\n"; }
function attach($db, $prefix = '')
{
$db->pgsqlSetNoticeCallback('disp'.$prefix);
}
require dirname(__FILE__) . '/issue78621.inc';
?>
--EXPECT--
NOTICE: I tampered your data, did you know?
ReNOTICE: I tampered your data, did you know?
array(1) {
[0]=>
array(1) {
["a"]=>
string(2) "oh"
}
}
Done

View File

@ -0,0 +1,58 @@
--TEST--
pgsqlSetNoticeCallback catches Postgres "raise notice".
--SKIPIF--
<?php
if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
require_once dirname(__FILE__) . '/config.inc';
PDOTest::skip();
?>
--FILE--
<?php
function disp($message) { echo trim($message)."\n"; }
function attach($db, $prefix = '')
{
global $flavor;
switch($flavor)
{
case 0:
$db->pgsqlSetNoticeCallback(function($message) use($prefix) { echo $prefix.trim($message)."\n"; });
// https://github.com/php/php-src/pull/4823#pullrequestreview-335623806
$eraseCallbackMemoryHere = (object)[1];
break;
case 1:
$closure = function($message) use($prefix) { echo $prefix.'('.get_class($this).')'.trim($message)."\n"; };
$db->pgsqlSetNoticeCallback($closure->bindTo(new \stdClass));
break;
}
}
echo "Testing with a simple inline closure:\n";
$flavor = 0;
require dirname(__FILE__) . '/issue78621.inc';
echo "Testing with a postbound closure object:\n";
++$flavor;
require dirname(__FILE__) . '/issue78621.inc';
?>
--EXPECT--
Testing with a simple inline closure:
NOTICE: I tampered your data, did you know?
ReNOTICE: I tampered your data, did you know?
array(1) {
[0]=>
array(1) {
["a"]=>
string(2) "oh"
}
}
Done
Testing with a postbound closure object:
(stdClass)NOTICE: I tampered your data, did you know?
Re(stdClass)NOTICE: I tampered your data, did you know?
array(1) {
[0]=>
array(1) {
["a"]=>
string(2) "oh"
}
}
Done

View File

@ -0,0 +1,65 @@
--TEST--
pgsqlSetNoticeCallback catches Postgres "raise notice".
--SKIPIF--
<?php
if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
require_once dirname(__FILE__) . '/config.inc';
PDOTest::skip();
?>
--FILE--
<?php
class Logger
{
public function disp($message) { echo trim($message)."\n"; }
public function dispRe($message) { echo "Re".trim($message)."\n"; }
public function __call(string $method, array $args)
{
$realMethod = strtr($method, [ 'whatever' => 'disp' ]);
echo "$method trampoline for $realMethod\n";
return call_user_func_array([ $this, $realMethod ], $args);
}
}
$logger = new Logger();
function attach($db, $prefix = '')
{
global $logger;
global $flavor;
switch($flavor)
{
case 0: $db->pgsqlSetNoticeCallback([ $logger, 'disp'.$prefix ]); break;
case 1: $db->pgsqlSetNoticeCallback([ $logger, 'whatever'.$prefix ]); break;
}
}
echo "Testing with method explicitely plugged:\n";
$flavor = 0;
require dirname(__FILE__) . '/issue78621.inc';
echo "Testing with a bit of magic:\n";
++$flavor;
require dirname(__FILE__) . '/issue78621.inc';
?>
--EXPECT--
Testing with method explicitely plugged:
NOTICE: I tampered your data, did you know?
ReNOTICE: I tampered your data, did you know?
array(1) {
[0]=>
array(1) {
["a"]=>
string(2) "oh"
}
}
Done
Testing with a bit of magic:
whatever trampoline for disp
NOTICE: I tampered your data, did you know?
whateverRe trampoline for dispRe
ReNOTICE: I tampered your data, did you know?
array(1) {
[0]=>
array(1) {
["a"]=>
string(2) "oh"
}
}
Done