Skip to content

Commit c265b90

Browse files
outtersgdevnexen
authored andcommitted
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
1 parent 182fee1 commit c265b90

10 files changed

+238
-4
lines changed

NEWS

+2
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ PHP NEWS
178178
. Fixed native float support with pdo_pgsql query results. (Yurunsoft)
179179
. Added class PdoPgsql. (danack, kocsismate)
180180
. Retrieve the memory usage of the query result resource. (KentarouTakeda)
181+
. Added PDO::pgsqlSetNoticeCallBack method to receive DB notices.
182+
(outtersg)
181183

182184
- PDO_SQLITE:
183185
. Added class PdoSqlite. (danack, kocsismate)

UPGRADING

+4
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,10 @@ PHP 8.4 UPGRADE NOTES
522522
. Added pcntl_getqos_class to get the QoS level (aka performance and related
523523
energy consumption) of the current process and pcntl_setqos_class to set it.
524524

525+
- PDO_PGSQL:
526+
. Added PDO::pgsqlSetNoticeCallback to allow a callback to be triggered on
527+
every notice sent (e.g. RAISE NOTICE).
528+
525529
- PGSQL:
526530
. Added pg_change_password to alter a given user's password. It handles
527531
transparently the password encryption from the database settings.

ext/pdo_pgsql/pgsql_driver.c

+45-3
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,16 @@ int _pdo_pgsql_error(pdo_dbh_t *dbh, pdo_stmt_t *stmt, int errcode, const char *
102102
}
103103
/* }}} */
104104

105-
static void _pdo_pgsql_notice(pdo_dbh_t *dbh, const char *message) /* {{{ */
105+
static void _pdo_pgsql_notice(void *context, const char *message) /* {{{ */
106106
{
107-
/* pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; */
107+
pdo_dbh_t * dbh = (pdo_dbh_t *)context;
108+
zend_fcall_info_cache *fc = ((pdo_pgsql_db_handle *)dbh->driver_data)->notice_callback;
109+
if (fc) {
110+
zval zarg;
111+
ZVAL_STRING(&zarg, message);
112+
zend_call_known_fcc(fc, NULL, 1, &zarg, NULL);
113+
zval_ptr_dtor_str(&zarg);
114+
}
108115
}
109116
/* }}} */
110117

@@ -125,6 +132,16 @@ static void pdo_pgsql_fetch_error_func(pdo_dbh_t *dbh, pdo_stmt_t *stmt, zval *i
125132
}
126133
/* }}} */
127134

135+
static void pdo_pgsql_cleanup_notice_callback(pdo_pgsql_db_handle *H) /* {{{ */
136+
{
137+
if (H->notice_callback) {
138+
zend_fcc_dtor(H->notice_callback);
139+
efree(H->notice_callback);
140+
H->notice_callback = NULL;
141+
}
142+
}
143+
/* }}} */
144+
128145
/* {{{ pdo_pgsql_create_lob_stream */
129146
static ssize_t pgsql_lob_write(php_stream *stream, const char *buf, size_t count)
130147
{
@@ -229,6 +246,7 @@ static void pgsql_handle_closer(pdo_dbh_t *dbh) /* {{{ */
229246
pefree(H->lob_streams, dbh->is_persistent);
230247
H->lob_streams = NULL;
231248
}
249+
pdo_pgsql_cleanup_notice_callback(H);
232250
if (H->server) {
233251
PQfinish(H->server);
234252
H->server = NULL;
@@ -1224,6 +1242,30 @@ PHP_METHOD(PDO_PGSql_Ext, pgsqlGetPid)
12241242
}
12251243
/* }}} */
12261244

1245+
/* {{{ proto void PDO::pgsqlSetNoticeCallback(mixed callback)
1246+
Sets a callback to receive DB notices (after client_min_messages has been set) */
1247+
PHP_METHOD(PDO_PGSql_Ext, pgsqlSetNoticeCallback)
1248+
{
1249+
zend_fcall_info fci = empty_fcall_info;
1250+
zend_fcall_info_cache fcc = empty_fcall_info_cache;
1251+
if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "F!", &fci, &fcc)) {
1252+
RETURN_THROWS();
1253+
}
1254+
1255+
pdo_dbh_t *dbh = Z_PDO_DBH_P(ZEND_THIS);
1256+
PDO_CONSTRUCT_CHECK;
1257+
1258+
pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
1259+
1260+
pdo_pgsql_cleanup_notice_callback(H);
1261+
1262+
if (ZEND_FCC_INITIALIZED(fcc)) {
1263+
H->notice_callback = emalloc(sizeof(zend_fcall_info_cache));
1264+
zend_fcc_dup(H->notice_callback, &fcc);
1265+
}
1266+
}
1267+
/* }}} */
1268+
12271269
static const zend_function_entry *pdo_pgsql_get_driver_methods(pdo_dbh_t *dbh, int kind)
12281270
{
12291271
switch (kind) {
@@ -1341,7 +1383,7 @@ static int pdo_pgsql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{
13411383
goto cleanup;
13421384
}
13431385

1344-
PQsetNoticeProcessor(H->server, (void(*)(void*,const char*))_pdo_pgsql_notice, (void *)&dbh);
1386+
PQsetNoticeProcessor(H->server, _pdo_pgsql_notice, (void *)dbh);
13451387

13461388
H->attached = 1;
13471389
H->pgoid = -1;

ext/pdo_pgsql/pgsql_driver.stub.php

+3
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ public function pgsqlGetNotify(int $fetchMode = PDO::FETCH_DEFAULT, int $timeout
3333

3434
/** @tentative-return-type */
3535
public function pgsqlGetPid(): int {}
36+
37+
/** @tentative-return-type */
38+
public function pgsqlSetNoticeCallback(?callable $callback): void {}
3639
}

ext/pdo_pgsql/pgsql_driver_arginfo.h

+7-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/pdo_pgsql/php_pdo_pgsql_int.h

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ typedef struct {
4646
bool disable_native_prepares; /* deprecated since 5.6 */
4747
bool disable_prepares;
4848
HashTable *lob_streams;
49+
zend_fcall_info_cache *notice_callback;
4950
} pdo_pgsql_db_handle;
5051

5152
typedef struct {

ext/pdo_pgsql/tests/issue78621.inc

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
3+
require_once dirname(__FILE__) . '/config.inc';
4+
$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
5+
6+
attach($db);
7+
8+
$db->beginTransaction();
9+
$db->exec("set client_min_messages to notice");
10+
$db->exec("create temporary table t (a varchar(3))");
11+
$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");
12+
$db->exec("create trigger hop before insert on t for each row execute procedure hey()");
13+
$db->exec("insert into t values ('ah')");
14+
attach($db, 'Re');
15+
$db->exec("delete from t");
16+
$db->exec("insert into t values ('ah')");
17+
$db->pgsqlSetNoticeCallback(null);
18+
$db->exec("delete from t");
19+
$db->exec("insert into t values ('ah')");
20+
var_dump($db->query("select * from t")->fetchAll(PDO::FETCH_ASSOC));
21+
echo "Done\n";
22+
$db->rollback();
23+
?>

ext/pdo_pgsql/tests/issue78621.phpt

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
pgsqlSetNoticeCallback catches Postgres "raise notice".
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
6+
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
7+
require_once dirname(__FILE__) . '/config.inc';
8+
PDOTest::skip();
9+
?>
10+
--FILE--
11+
<?php
12+
function disp($message) { echo trim($message)."\n"; }
13+
function dispRe($message) { echo "Re".trim($message)."\n"; }
14+
function attach($db, $prefix = '')
15+
{
16+
$db->pgsqlSetNoticeCallback('disp'.$prefix);
17+
}
18+
require dirname(__FILE__) . '/issue78621.inc';
19+
?>
20+
--EXPECT--
21+
NOTICE: I tampered your data, did you know?
22+
ReNOTICE: I tampered your data, did you know?
23+
array(1) {
24+
[0]=>
25+
array(1) {
26+
["a"]=>
27+
string(2) "oh"
28+
}
29+
}
30+
Done
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
--TEST--
2+
pgsqlSetNoticeCallback catches Postgres "raise notice".
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
6+
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
7+
require_once dirname(__FILE__) . '/config.inc';
8+
PDOTest::skip();
9+
?>
10+
--FILE--
11+
<?php
12+
function disp($message) { echo trim($message)."\n"; }
13+
function attach($db, $prefix = '')
14+
{
15+
global $flavor;
16+
switch($flavor)
17+
{
18+
case 0:
19+
$db->pgsqlSetNoticeCallback(function($message) use($prefix) { echo $prefix.trim($message)."\n"; });
20+
// https://github.com/php/php-src/pull/4823#pullrequestreview-335623806
21+
$eraseCallbackMemoryHere = (object)[1];
22+
break;
23+
case 1:
24+
$closure = function($message) use($prefix) { echo $prefix.'('.get_class($this).')'.trim($message)."\n"; };
25+
$db->pgsqlSetNoticeCallback($closure->bindTo(new \stdClass));
26+
break;
27+
}
28+
}
29+
echo "Testing with a simple inline closure:\n";
30+
$flavor = 0;
31+
require dirname(__FILE__) . '/issue78621.inc';
32+
echo "Testing with a postbound closure object:\n";
33+
++$flavor;
34+
require dirname(__FILE__) . '/issue78621.inc';
35+
?>
36+
--EXPECT--
37+
Testing with a simple inline closure:
38+
NOTICE: I tampered your data, did you know?
39+
ReNOTICE: I tampered your data, did you know?
40+
array(1) {
41+
[0]=>
42+
array(1) {
43+
["a"]=>
44+
string(2) "oh"
45+
}
46+
}
47+
Done
48+
Testing with a postbound closure object:
49+
(stdClass)NOTICE: I tampered your data, did you know?
50+
Re(stdClass)NOTICE: I tampered your data, did you know?
51+
array(1) {
52+
[0]=>
53+
array(1) {
54+
["a"]=>
55+
string(2) "oh"
56+
}
57+
}
58+
Done
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
--TEST--
2+
pgsqlSetNoticeCallback catches Postgres "raise notice".
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
6+
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
7+
require_once dirname(__FILE__) . '/config.inc';
8+
PDOTest::skip();
9+
?>
10+
--FILE--
11+
<?php
12+
class Logger
13+
{
14+
public function disp($message) { echo trim($message)."\n"; }
15+
public function dispRe($message) { echo "Re".trim($message)."\n"; }
16+
public function __call(string $method, array $args)
17+
{
18+
$realMethod = strtr($method, [ 'whatever' => 'disp' ]);
19+
echo "$method trampoline for $realMethod\n";
20+
return call_user_func_array([ $this, $realMethod ], $args);
21+
}
22+
}
23+
$logger = new Logger();
24+
function attach($db, $prefix = '')
25+
{
26+
global $logger;
27+
global $flavor;
28+
switch($flavor)
29+
{
30+
case 0: $db->pgsqlSetNoticeCallback([ $logger, 'disp'.$prefix ]); break;
31+
case 1: $db->pgsqlSetNoticeCallback([ $logger, 'whatever'.$prefix ]); break;
32+
}
33+
}
34+
echo "Testing with method explicitely plugged:\n";
35+
$flavor = 0;
36+
require dirname(__FILE__) . '/issue78621.inc';
37+
echo "Testing with a bit of magic:\n";
38+
++$flavor;
39+
require dirname(__FILE__) . '/issue78621.inc';
40+
?>
41+
--EXPECT--
42+
Testing with method explicitely plugged:
43+
NOTICE: I tampered your data, did you know?
44+
ReNOTICE: I tampered your data, did you know?
45+
array(1) {
46+
[0]=>
47+
array(1) {
48+
["a"]=>
49+
string(2) "oh"
50+
}
51+
}
52+
Done
53+
Testing with a bit of magic:
54+
whatever trampoline for disp
55+
NOTICE: I tampered your data, did you know?
56+
whateverRe trampoline for dispRe
57+
ReNOTICE: I tampered your data, did you know?
58+
array(1) {
59+
[0]=>
60+
array(1) {
61+
["a"]=>
62+
string(2) "oh"
63+
}
64+
}
65+
Done

0 commit comments

Comments
 (0)