Cooperative pg_query() for fiber-interop/api: async\pg\query(), query_params(), start() and connect() on any adapter, waiting on the libpq socket.
Requires
- php: ^8.1
- ext-pgsql: *
- fiber-interop/api: ^0.2
Requires (Dev)
- fiber-interop/amp: *
- fiber-interop/phasync: *
- fiber-interop/react: *
- fiber-interop/revolt: *
- fiber-interop/swoole: *
- phpunit/phpunit: ^10.5 || ^11
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-14 06:46:23 UTC
README
Cooperative pg_query(), pg_query_params(), pg_prepare(), pg_execute() and pg_connect() for
fiber-interop/api: inside async\run() a query suspends only
the calling task, on whichever adapter is installed.
composer require fiber-interop/pg
use function async\{run, go, await_all}; $rows = run(fn() => await_all(array_map( fn($id) => go(function () use ($id) { $c = async\pg\connect('host=db user=app dbname=app', PGSQL_CONNECT_FORCE_NEW); $r = async\pg\query_params($c, 'SELECT * FROM users WHERE id = $1', [$id]); return $r === false ? pg_last_error($c) : pg_fetch_assoc($r); // same contract as pg_query_params() }), [1, 2, 3], )));
API
async\pg\query(\PgSql\Connection $c, string $sql): \PgSql\Result|falseandasync\pg\query_params(\PgSql\Connection $c, string $sql, array $params): \PgSql\Result|false— the contracts ofpg_query()/pg_query_params(): the (last) result, orfalsewithpg_last_error($c)set; the connection stays usable after a failed query. Outsideasync\run()they are the plain blocking functions. Inside, a failure raises no warning (the original emits "Query failed").async\pg\prepare(\PgSql\Connection $c, string $name, string $sql): \PgSql\Result|falseandasync\pg\execute(\PgSql\Connection $c, string $name, array $params): \PgSql\Result|false— the same forpg_prepare()/pg_execute(), sent withpg_send_prepare()/pg_send_execute().async\pg\start(\PgSql\Connection $c, string $sql, ?array $params = null): object— sends the query now and returns an awaitable (repeatableasync\await()) resolving to whatquery()(orquery_params()when$paramsis given) returns.query()isawait(start()).async\pg\connect(string $conninfo, int $flags = 0): \PgSql\Connection|false—pg_connect(); insideasync\run()it connects withPGSQL_CONNECT_ASYNCandpg_connect_poll(), waiting on the socket between steps. A failure returnsfalsewithout a warning. As withpg_connect(), passPGSQL_CONNECT_FORCE_NEWto get a separate connection per task.async\pg\Driver— one method,start(\PgSql\Connection, string, ?array): object.async\pg\setup(Driver),async\pg\register(callable $factory, string $package),async\pg\driver()— same semantics as fiber-interop/curl's registry:setup()wins; a single registered candidate is instantiated lazily; two candidates withoutsetup()throwasync\NotConfiguredException; with none, the defaultSocketDriveris used. Adapters writeasync\pg\Registry::$candidates[...]directly from theirregister.php, guarded byinterface_exists(\async\pg\Driver::class)and their own runtime check.
How it waits: on a real file descriptor
SocketDriver sends with pg_send_query() / pg_send_query_params() and then waits with
async\readable(pg_socket($c)), calling pg_consume_input() until pg_connection_busy() is false,
and collects the results with pg_get_result() (keeping the last, as pg_query() does). libpq hands
out its socket, so the event loop watches it directly: no polling, no timers, no threads. Contrast
fiber-interop/curl, where PHP exposes no descriptor for curl_multi and the driver has to poll.
libpq allows one query in flight per connection. A query started on a connection that is already running one is queued and sent when that one completes, so two tasks may share a connection and get their results in order; for parallelism, use one connection per task.
Native drivers
| Runtime | Driver |
|---|---|
| Revolt, AMPHP, ReactPHP, phasync (with or without the extension), Swoole | SocketDriver |
Swoole's SWOOLE_HOOK_ALL covers pdo_pgsql (SWOOLE_HOOK_PDO_PGSQL), not ext-pgsql, so a plain
pg_query() blocks the worker there; SocketDriver is cooperative on it because pg_socket() is
an ordinary descriptor Swoole's readable() can wait on. The phasync extension does make plain
pg_query() cooperative (it redirects libpq's poll()), so a phasync native driver
(go(fn() => pg_query(...))) is possible; SocketDriver works there too and is what is tested.
Limits
- A timed-out or cancelled
await()does not abort the query; it runs to completion (the connection is busy until then, and later queries queue behind it) and its result is discarded unless awaited again. - Inside
connect(), host name resolution happens inside libpq and blocks; use an IP address or a local resolver cache if that matters.pg_send_query()flushes large queries with a short blocking loop inside the pgsql extension. - Do not mix
async\pgwith plainpg_query()on the same connection while anasync\pgquery is in flight.
Tests
The suite needs a PostgreSQL server; PG_DSN is its libpq conninfo, defaulting to
host=127.0.0.1 port=15432 user=fi password=fi dbname=fi, which matches:
docker run -d --name fi-pg -p 127.0.0.1:15432:5432 -e POSTGRES_USER=fi -e POSTGRES_PASSWORD=fi -e POSTGRES_DB=fi postgres:16
FI_RUNTIME=revolt|amp|react|phasync|swoole picks the adapter:
FI_RUNTIME=amp vendor/bin/phpunit
FI_RUNTIME=phasync php -d extension=phasync.so vendor/bin/phpunit