1
0
mirror of https://github.com/danog/postgres.git synced 2024-12-14 02:17:27 +01:00
postgres/lib/PgSqlHandle.php

412 lines
12 KiB
PHP
Raw Normal View History

2016-12-30 06:21:17 +01:00
<?php
2016-09-14 16:27:39 +02:00
namespace Amp\Postgres;
use Amp\CallableMaker;
use Amp\Deferred;
use Amp\Emitter;
use Amp\Loop;
use Amp\Promise;
use Amp\Success;
use function Amp\call;
2016-09-14 16:27:39 +02:00
class PgSqlHandle implements Handle {
2016-09-14 16:27:39 +02:00
use CallableMaker;
2017-05-16 06:28:37 +02:00
const DIAGNOSTIC_CODES = [
\PGSQL_DIAG_SEVERITY => "severity",
\PGSQL_DIAG_SQLSTATE => "sqlstate",
\PGSQL_DIAG_MESSAGE_PRIMARY => "message_primary",
\PGSQL_DIAG_MESSAGE_DETAIL => "message_detail",
\PGSQL_DIAG_MESSAGE_HINT => "message_hint",
\PGSQL_DIAG_STATEMENT_POSITION => "statement_position",
\PGSQL_DIAG_INTERNAL_POSITION => "internal_position",
\PGSQL_DIAG_INTERNAL_QUERY => "internal_query",
\PGSQL_DIAG_CONTEXT => "context",
\PGSQL_DIAG_SOURCE_FILE => "source_file",
\PGSQL_DIAG_SOURCE_LINE => "source_line",
\PGSQL_DIAG_SOURCE_FUNCTION => "source_function",
];
2016-09-14 16:27:39 +02:00
/** @var resource PostgreSQL connection handle. */
private $handle;
/** @var \Amp\Deferred|null */
private $deferred;
2016-09-14 16:27:39 +02:00
/** @var string */
private $poll;
/** @var string */
private $await;
/** @var callable */
private $executeCallback;
2017-05-16 06:28:37 +02:00
/** @var callable */
private $deallocateCallback;
/** @var \Amp\Emitter[] */
2016-09-19 18:12:32 +02:00
private $listeners = [];
2017-05-16 06:28:37 +02:00
2016-09-19 18:12:32 +02:00
/** @var callable */
private $unlisten;
2017-05-16 06:28:37 +02:00
/** @var \Amp\Postgres\Internal\StatementStorage[] */
private $statements = [];
2016-09-14 16:27:39 +02:00
/**
* Connection constructor.
*
* @param resource $handle PostgreSQL connection handle.
* @param resource $socket PostgreSQL connection stream socket.
*/
public function __construct($handle, $socket) {
$this->handle = $handle;
$handle = &$this->handle;
$deferred = &$this->deferred;
2016-09-19 18:12:32 +02:00
$listeners = &$this->listeners;
2017-05-16 06:28:37 +02:00
$this->poll = Loop::onReadable($socket, static function ($watcher) use (&$deferred, &$listeners, &$handle) {
2017-06-22 02:12:45 +02:00
if (!\pg_consume_input($handle)) {
$handle = null; // Marks connection as dead.
Loop::disable($watcher);
$exception = new ConnectionException(\pg_last_error($handle));
foreach ($listeners as $listener) {
$listener->fail($exception);
}
2017-06-22 02:12:45 +02:00
if ($deferred !== null) {
$deferred->fail($exception);
2017-06-22 02:12:45 +02:00
}
2017-06-22 02:12:45 +02:00
return;
}
2017-05-16 06:28:37 +02:00
while ($result = \pg_get_notify($handle, \PGSQL_ASSOC)) {
$channel = $result["message"];
if (!isset($listeners[$channel])) {
continue;
2016-09-19 18:12:32 +02:00
}
$notification = new Notification;
$notification->channel = $channel;
$notification->pid = $result["pid"];
$notification->payload = $result["payload"];
$listeners[$channel]->emit($notification);
2016-09-19 18:12:32 +02:00
}
2017-05-16 06:28:37 +02:00
if ($deferred === null) {
return; // No active query, only notification listeners.
}
2017-05-16 06:28:37 +02:00
2017-06-22 02:12:45 +02:00
if (\pg_connection_busy($handle)) {
return;
}
2016-09-14 16:27:39 +02:00
2017-06-22 02:12:45 +02:00
$deferred->resolve(\pg_get_result($handle));
if (!$deferred && empty($listeners)) {
Loop::disable($watcher);
2016-09-14 16:27:39 +02:00
}
});
$this->await = Loop::onWritable($socket, static function ($watcher) use (&$deferred, $handle) {
$flush = \pg_flush($handle);
if ($flush === 0) {
2016-09-14 16:27:39 +02:00
return; // Not finished sending data, listen again.
}
2017-05-16 06:28:37 +02:00
2016-09-14 16:27:39 +02:00
Loop::disable($watcher);
if ($flush === false) {
$deferred->fail(new FailureException(\pg_last_error($handle)));
}
});
2017-05-16 06:28:37 +02:00
2016-09-14 16:27:39 +02:00
Loop::disable($this->poll);
Loop::disable($this->await);
$this->executeCallback = $this->callableFromInstanceMethod("sendExecute");
$this->deallocateCallback = $this->callableFromInstanceMethod("sendDeallocate");
2016-09-19 18:12:32 +02:00
$this->unlisten = $this->callableFromInstanceMethod("unlisten");
2016-09-14 16:27:39 +02:00
}
/**
* Frees Io watchers from loop.
*/
public function __destruct() {
if (\is_resource($this->handle)) {
\pg_close($this->handle);
}
2017-05-16 06:28:37 +02:00
2016-09-14 16:27:39 +02:00
Loop::cancel($this->poll);
Loop::cancel($this->await);
}
/**
* {@inheritdoc}
*/
public function isAlive(): bool {
return $this->handle !== null;
}
/**
2016-09-14 16:27:39 +02:00
* @param callable $function Function name to execute.
* @param mixed ...$args Arguments to pass to function.
*
* @return \Generator
*
* @resolve resource
*
* @throws \Amp\Postgres\FailureException
*/
private function send(callable $function, ...$args): \Generator {
while ($this->deferred) {
try {
yield $this->deferred->promise();
} catch (\Throwable $exception) {
// Ignore failure from another operation.
}
2016-09-14 16:27:39 +02:00
}
2017-11-06 05:36:22 +01:00
if (!\is_resource($this->handle)) {
throw new ConnectionException("The connection to the database has been lost");
}
2016-09-14 16:27:39 +02:00
$result = $function($this->handle, ...$args);
if ($result === false) {
throw new FailureException(\pg_last_error($this->handle));
}
$this->deferred = new Deferred;
2016-09-14 16:27:39 +02:00
Loop::enable($this->poll);
if (0 === $result) {
Loop::enable($this->await);
}
try {
2016-11-15 18:06:21 +01:00
$result = yield $this->deferred->promise();
2016-09-14 16:27:39 +02:00
} finally {
$this->deferred = null;
2016-09-14 16:27:39 +02:00
}
return $result;
}
2017-05-16 06:28:37 +02:00
2016-09-14 16:27:39 +02:00
/**
* @param resource $result PostgreSQL result resource.
*
* @return \Amp\Postgres\CommandResult|\Amp\Postgres\ResultSet
2016-09-14 16:27:39 +02:00
*
* @throws \Amp\Postgres\FailureException
* @throws \Amp\Postgres\QueryError
*/
2017-05-16 06:14:02 +02:00
private function createResult($result) {
2016-09-14 16:27:39 +02:00
switch (\pg_result_status($result, \PGSQL_STATUS_LONG)) {
case \PGSQL_EMPTY_QUERY:
throw new QueryError("Empty query string");
case \PGSQL_COMMAND_OK:
return new PgSqlCommandResult($result);
case \PGSQL_TUPLES_OK:
return new PgSqlResultSet($result);
2016-09-14 16:27:39 +02:00
case \PGSQL_NONFATAL_ERROR:
case \PGSQL_FATAL_ERROR:
$diagnostics = [];
foreach (self::DIAGNOSTIC_CODES as $fieldCode => $desciption) {
$diagnostics[$desciption] = \pg_result_error_field($result, $fieldCode);
}
throw new QueryExecutionError(\pg_result_error($result), $diagnostics);
2016-09-14 16:27:39 +02:00
case \PGSQL_BAD_RESPONSE:
throw new FailureException(\pg_result_error($result));
default:
// @codeCoverageIgnoreStart
2016-09-14 16:27:39 +02:00
throw new FailureException("Unknown result status");
// @codeCoverageIgnoreEnd
2016-09-14 16:27:39 +02:00
}
}
2017-05-16 06:28:37 +02:00
2016-11-15 18:06:21 +01:00
private function sendExecute(string $name, array $params): Promise {
2017-05-16 06:14:02 +02:00
return call(function () use ($name, $params) {
return $this->createResult(yield from $this->send("pg_send_execute", $name, $params));
});
2016-09-14 16:27:39 +02:00
}
private function sendDeallocate(string $name) {
\assert(isset($this->statements[$name]), "Named statement not found when deallocating");
$storage = $this->statements[$name];
if (--$storage->count) {
return;
}
unset($this->statements[$name]);
Promise\rethrow($this->query(\sprintf("DEALLOCATE %s", $name)));
}
2016-09-14 16:27:39 +02:00
/**
* {@inheritdoc}
*/
2016-11-15 18:06:21 +01:00
public function query(string $sql): Promise {
2017-05-16 06:14:02 +02:00
return call(function () use ($sql) {
return $this->createResult(yield from $this->send("pg_send_query", $sql));
});
2016-09-14 16:27:39 +02:00
}
/**
* {@inheritdoc}
*/
public function execute(string $sql, array $params = []): Promise {
2017-05-16 06:14:02 +02:00
return call(function () use ($sql, $params) {
return $this->createResult(yield from $this->send("pg_send_query_params", $sql, $params));
2017-05-16 06:14:02 +02:00
});
2016-09-14 16:27:39 +02:00
}
/**
* {@inheritdoc}
*/
2016-11-15 18:06:21 +01:00
public function prepare(string $sql): Promise {
$name = self::STATEMENT_NAME_PREFIX . \sha1($sql);
if (isset($this->statements[$name])) {
$storage = $this->statements[$name];
++$storage->count;
if ($storage->promise) {
return $storage->promise;
}
return new Success(new PgSqlStatement($name, $sql, $this->executeCallback, $this->deallocateCallback));
}
$this->statements[$name] = $storage = new Internal\StatementStorage;
$promise = $storage->promise = call(function () use ($name, $sql) {
/** @var resource $result PostgreSQL result resource. */
$result = yield from $this->send("pg_send_prepare", $name, $sql);
switch (\pg_result_status($result, \PGSQL_STATUS_LONG)) {
case \PGSQL_COMMAND_OK:
return new PgSqlStatement($name, $sql, $this->executeCallback, $this->deallocateCallback);
case \PGSQL_NONFATAL_ERROR:
case \PGSQL_FATAL_ERROR:
$diagnostics = [];
foreach (self::DIAGNOSTIC_CODES as $fieldCode => $desciption) {
$diagnostics[$desciption] = \pg_result_error_field($result, $fieldCode);
}
throw new QueryExecutionError(\pg_result_error($result), $diagnostics);
case \PGSQL_BAD_RESPONSE:
throw new FailureException(\pg_result_error($result));
default:
// @codeCoverageIgnoreStart
throw new FailureException("Unknown result status");
// @codeCoverageIgnoreEnd
}
2016-09-14 16:27:39 +02:00
});
$promise->onResolve(function ($exception) use ($storage, $name) {
2017-07-31 07:52:56 +02:00
if ($exception) {
unset($this->statements[$name]);
return;
}
$storage->promise = null;
});
return $promise;
2016-09-14 16:27:39 +02:00
}
2017-05-16 06:28:37 +02:00
/**
* {@inheritdoc}
*/
2016-11-15 18:06:21 +01:00
public function notify(string $channel, string $payload = ""): Promise {
if ($payload === "") {
return $this->query(\sprintf("NOTIFY %s", $this->quoteName($channel)));
}
2017-05-16 06:28:37 +02:00
return $this->query(\sprintf("NOTIFY %s, %s", $this->quoteName($channel), $this->quoteString($payload)));
}
2017-05-16 06:28:37 +02:00
2016-09-19 18:12:32 +02:00
/**
* {@inheritdoc}
*/
2016-11-15 18:06:21 +01:00
public function listen(string $channel): Promise {
2017-05-16 06:14:02 +02:00
return call(function () use ($channel) {
if (isset($this->listeners[$channel])) {
throw new QueryError(\sprintf("Already listening on channel '%s'", $channel));
}
$this->listeners[$channel] = $emitter = new Emitter;
try {
yield $this->query(\sprintf("LISTEN %s", $this->quoteName($channel)));
} catch (\Throwable $exception) {
unset($this->listeners[$channel]);
throw $exception;
}
2017-05-16 06:14:02 +02:00
Loop::enable($this->poll);
2017-05-16 06:14:02 +02:00
return new Listener($emitter->iterate(), $channel, $this->unlisten);
2016-09-19 18:12:32 +02:00
});
}
2017-05-16 06:28:37 +02:00
2016-09-19 18:12:32 +02:00
/**
* @param string $channel
*
* @return \Amp\Promise
2016-09-19 18:12:32 +02:00
*
* @throws \Error
*/
2016-11-15 18:06:21 +01:00
private function unlisten(string $channel): Promise {
\assert(isset($this->listeners[$channel]), "Not listening on that channel");
2017-05-16 06:28:37 +02:00
$emitter = $this->listeners[$channel];
2016-09-19 18:12:32 +02:00
unset($this->listeners[$channel]);
2017-05-16 06:28:37 +02:00
2017-11-06 05:36:22 +01:00
if (!\is_resource($this->handle)) {
$promise = new Success; // Connection already closed.
} else {
$promise = $this->query(\sprintf("UNLISTEN %s", $this->quoteName($channel)));
}
2017-05-16 06:28:37 +02:00
$promise->onResolve([$emitter, "complete"]);
2016-11-15 18:06:21 +01:00
return $promise;
2016-09-19 18:12:32 +02:00
}
/**
* {@inheritdoc}
*/
public function quoteString(string $data): string {
2017-11-06 05:36:22 +01:00
if (!\is_resource($this->handle)) {
throw new ConnectionException("The connection to the database has been lost");
}
return \pg_escape_literal($this->handle, $data);
}
/**
* {@inheritdoc}
*/
public function quoteName(string $name): string {
2017-11-06 05:36:22 +01:00
if (!\is_resource($this->handle)) {
throw new ConnectionException("The connection to the database has been lost");
}
return \pg_escape_identifier($this->handle, $name);
}
2016-09-14 16:27:39 +02:00
}