diff --git a/examples/5-bytea.php b/examples/5-bytea.php new file mode 100644 index 0000000..ae5c093 --- /dev/null +++ b/examples/5-bytea.php @@ -0,0 +1,32 @@ +#!/usr/bin/env php +query('DROP TABLE IF EXISTS test'); + +$transaction = $pool->beginTransaction(); + +$transaction->query('CREATE TABLE test (value BYTEA)'); + +$statement = $transaction->prepare('INSERT INTO test VALUES (?)'); + +$statement->execute([new ByteA($a = \random_bytes(10))]); +$statement->execute([new ByteA($b = \random_bytes(10))]); +$statement->execute([new ByteA($c = \random_bytes(10))]); + +$result = $transaction->execute('SELECT * FROM test WHERE value = :value', ['value' => new ByteA($a)]); + +foreach ($result as $row) { + assert($row['value'] === $a); + var_dump(bin2hex($row['value'])); +} + +$transaction->rollback(); diff --git a/src/ByteA.php b/src/ByteA.php new file mode 100644 index 0000000..02b44cb --- /dev/null +++ b/src/ByteA.php @@ -0,0 +1,16 @@ +data; + } +} diff --git a/src/Internal/AbstractHandle.php b/src/Internal/AbstractHandle.php index 7ed342d..e925ae1 100644 --- a/src/Internal/AbstractHandle.php +++ b/src/Internal/AbstractHandle.php @@ -4,6 +4,7 @@ use Amp\DeferredFuture; use Amp\Pipeline\Queue; +use Amp\Postgres\ByteA; use Amp\Postgres\PostgresHandle; use Amp\Postgres\PostgresResult; use Amp\Postgres\PostgresStatement; @@ -85,4 +86,13 @@ protected static function shutdown( $onClose->complete(); } } + + protected function escapeParams(array $params): array + { + return \array_map(fn (mixed $param) => match (true) { + $param instanceof ByteA => $this->escapeByteA($param->getData()), + \is_array($param) => $this->escapeParams($param), + default => $param, + }, $params); + } } diff --git a/src/Internal/PgSqlHandle.php b/src/Internal/PgSqlHandle.php index 0d3b4a4..c42bb40 100644 --- a/src/Internal/PgSqlHandle.php +++ b/src/Internal/PgSqlHandle.php @@ -286,7 +286,8 @@ private function fetchNextResult(string $sql): ?PostgresResult public function statementExecute(string $name, array $params): PostgresResult { \assert(isset($this->statements[$name]), "Named statement not found when executing"); - return $this->createResult($this->send(\pg_send_execute(...), $name, $params), $this->statements[$name]->sql); + $result = $this->send(\pg_send_execute(...), $name, \array_map(cast(...), $this->escapeParams($params))); + return $this->createResult($result, $this->statements[$name]->sql); } /** @@ -318,6 +319,15 @@ public function statementDeallocate(string $name): void $storage->future->ignore(); } + public function escapeByteA(string $data): string + { + if ($this->handle === null) { + throw new \Error("The connection to the database has been closed"); + } + + return \pg_escape_bytea($this->handle, $data); + } + public function query(string $sql): PostgresResult { if ($this->handle === null) { @@ -336,7 +346,13 @@ public function execute(string $sql, array $params = []): PostgresResult $sql = parseNamedParams($sql, $names); $params = replaceNamedParams($params, $names); - return $this->createResult($this->send(\pg_send_query_params(...), $sql, $params), $sql); + $result = $this->send( + pg_send_query_params(...), + $sql, + \array_map(cast(...), $this->escapeParams($params)) + ); + + return $this->createResult($result, $sql); } public function prepare(string $sql): PostgresStatement diff --git a/src/Internal/PgSqlResultIterator.php b/src/Internal/PgSqlResultIterator.php index b5372b3..aa25586 100644 --- a/src/Internal/PgSqlResultIterator.php +++ b/src/Internal/PgSqlResultIterator.php @@ -88,7 +88,10 @@ private function cast(int $oid, ?string $value): array|bool|int|float|string|nul 790 => $value, // money includes currency symbol as string default => (int) $value, // All other numeric types cast to an integer }, - default => $value, // Return a string for all other types + default => match ($oid) { // String + 17 => \pg_unescape_bytea($value), + default => $value, // Return a string for all other types + }, }; } } diff --git a/src/Internal/PostgresConnectionTransaction.php b/src/Internal/PostgresConnectionTransaction.php index 1059851..401f2ec 100644 --- a/src/Internal/PostgresConnectionTransaction.php +++ b/src/Internal/PostgresConnectionTransaction.php @@ -257,4 +257,13 @@ public function quoteName(string $name): string $this->assertOpen(); return $this->handle->quoteName($name); } + + /** + * @throws TransactionError If the transaction has been committed or rolled back. + */ + public function escapeByteA(string $data): string + { + $this->assertOpen(); + return $this->handle->escapeByteA($data); + } } diff --git a/src/Internal/PostgresPooledTransaction.php b/src/Internal/PostgresPooledTransaction.php index cb06031..e8756bb 100644 --- a/src/Internal/PostgresPooledTransaction.php +++ b/src/Internal/PostgresPooledTransaction.php @@ -76,4 +76,9 @@ public function quoteName(string $name): string { return $this->transaction->quoteName($name); } + + public function escapeByteA(string $data): string + { + return $this->transaction->escapeByteA($data); + } } diff --git a/src/Internal/PqBufferedResultSet.php b/src/Internal/PqBufferedResultSet.php index 764d841..890398d 100644 --- a/src/Internal/PqBufferedResultSet.php +++ b/src/Internal/PqBufferedResultSet.php @@ -37,7 +37,7 @@ private static function generate(pq\Result $result): \Generator $position = 0; while (++$position <= $result->numRows) { - $result->autoConvert = pq\Result::CONV_SCALAR | pq\Result::CONV_ARRAY; + $result->autoConvert = pq\Result::CONV_SCALAR | pq\Result::CONV_ARRAY | pq\Result::CONV_BYTEA; yield $result->fetchRow(pq\Result::FETCH_ASSOC); } } diff --git a/src/Internal/PqHandle.php b/src/Internal/PqHandle.php index b957d8a..3694e4e 100644 --- a/src/Internal/PqHandle.php +++ b/src/Internal/PqHandle.php @@ -300,7 +300,11 @@ public function statementExecute(string $name, array $params): PostgresResult throw new SqlException('Statement unexpectedly closed before being executed'); } - return $this->send($storage->sql, $statement->execAsync(...), $params); + return $this->send( + $storage->sql, + $statement->execAsync(...), + \array_map(cast(...), $this->escapeParams($params)), + ); } /** @@ -332,6 +336,15 @@ public function statementDeallocate(string $name): void }); } + public function escapeByteA(string $data): string + { + if (!$this->handle) { + throw new \Error("The connection to the database has been closed"); + } + + return $this->handle->escapeBytea($data); + } + public function query(string $sql): PostgresResult { if (!$this->handle) { @@ -350,7 +363,12 @@ public function execute(string $sql, array $params = []): PostgresResult $sql = parseNamedParams($sql, $names); $params = replaceNamedParams($params, $names); - return $this->send($sql, $this->handle->execParamsAsync(...), $sql, $params); + return $this->send( + $sql, + $this->handle->execParamsAsync(...), + $sql, + \array_map(cast(...), $this->escapeParams($params)), + ); } public function prepare(string $sql): PostgresStatement diff --git a/src/Internal/PqUnbufferedResultSet.php b/src/Internal/PqUnbufferedResultSet.php index fd1391e..072d625 100644 --- a/src/Internal/PqUnbufferedResultSet.php +++ b/src/Internal/PqUnbufferedResultSet.php @@ -36,7 +36,7 @@ public function __construct( private static function generate(\Closure $fetch, pq\Result $result): \Generator { do { - $result->autoConvert = pq\Result::CONV_SCALAR | pq\Result::CONV_ARRAY; + $result->autoConvert = pq\Result::CONV_SCALAR | pq\Result::CONV_ARRAY | pq\Result::CONV_BYTEA; yield $result->fetchRow(pq\Result::FETCH_ASSOC); $result = $fetch(); } while ($result instanceof pq\Result); diff --git a/src/Internal/functions.php b/src/Internal/functions.php index d90b037..d3ac97e 100644 --- a/src/Internal/functions.php +++ b/src/Internal/functions.php @@ -67,7 +67,7 @@ function parseNamedParams(string $sql, ?array &$names): string * @param array $params User-provided array of statement parameters. * @param list $names Array generated by the $names param of {@see parseNamedParams()}. * - * @return list + * @return list * * @throws \Error If the $param array does not contain a key corresponding to a named parameter. */ @@ -85,7 +85,7 @@ function replaceNamedParams(array $params, array $names): array throw new \Error($message); } - $values[] = cast($params[$name]); + $values[] = $params[$name]; } return $values; diff --git a/src/PostgresConnection.php b/src/PostgresConnection.php index 028a15b..b596302 100644 --- a/src/PostgresConnection.php +++ b/src/PostgresConnection.php @@ -129,4 +129,9 @@ final public function quoteName(string $name): string { return $this->handle->quoteName($name); } + + final public function escapeByteA(string $data): string + { + return $this->handle->escapeByteA($data); + } } diff --git a/src/PostgresHandle.php b/src/PostgresHandle.php index c4c6c76..942314a 100644 --- a/src/PostgresHandle.php +++ b/src/PostgresHandle.php @@ -9,7 +9,7 @@ interface PostgresHandle extends PostgresReceiver, PostgresQuoter /** * Execute the statement with the given name and parameters. * - * @param list $params List of statement parameters, indexed starting at 0. + * @param list $params List of statement parameters, indexed starting at 0. */ public function statementExecute(string $name, array $params): PostgresResult; diff --git a/src/PostgresQuoter.php b/src/PostgresQuoter.php index 6976440..3e4d149 100644 --- a/src/PostgresQuoter.php +++ b/src/PostgresQuoter.php @@ -26,4 +26,9 @@ public function quoteString(string $data): string; * @throws \Error If the connection to the database has been closed. */ public function quoteName(string $name): string; + + /** + * Escapes a binary string to be used as BYTEA data. + */ + public function escapeByteA(string $data): string; } diff --git a/test/AbstractConnectTest.php b/test/AbstractConnectTest.php index 46c2053..3ea0fed 100644 --- a/test/AbstractConnectTest.php +++ b/test/AbstractConnectTest.php @@ -3,59 +3,14 @@ namespace Amp\Postgres\Test; use Amp\Cancellation; -use Amp\CancelledException; -use Amp\DeferredCancellation; use Amp\PHPUnit\AsyncTestCase; use Amp\Postgres\PostgresConfig; use Amp\Postgres\PostgresConnection; -use Amp\Sql\SqlException; -use Amp\TimeoutCancellation; abstract class AbstractConnectTest extends AsyncTestCase { - abstract public function connect(PostgresConfig $connectionConfig, Cancellation $cancellation = null): PostgresConnection; - - public function testConnect() - { - $connection = $this->connect( - PostgresConfig::fromString('host=localhost user=postgres password=postgres'), - new TimeoutCancellation(1) - ); - $this->assertInstanceOf(PostgresConnection::class, $connection); - } - - /** - * @depends testConnect - */ - public function testConnectCancellationBeforeConnect() - { - $this->expectException(CancelledException::class); - - $source = new DeferredCancellation; - $cancellation = $source->getCancellation(); - $source->cancel(); - $this->connect(PostgresConfig::fromString('host=localhost user=postgres password=postgres'), $cancellation); - } - - /** - * @depends testConnectCancellationBeforeConnect - */ - public function testConnectCancellationAfterConnect() - { - $source = new DeferredCancellation; - $cancellation = $source->getCancellation(); - $connection = $this->connect(PostgresConfig::fromString('host=localhost user=postgres password=postgres'), $cancellation); - $this->assertInstanceOf(PostgresConnection::class, $connection); - $source->cancel(); - } - - /** - * @depends testConnectCancellationBeforeConnect - */ - public function testConnectInvalidUser() - { - $this->expectException(SqlException::class); - - $this->connect(PostgresConfig::fromString('host=localhost user=invalid password=invalid'), new TimeoutCancellation(100)); - } + abstract public function connect( + PostgresConfig $connectionConfig, + ?Cancellation $cancellation = null + ): PostgresConnection; } diff --git a/test/AbstractCreateConnectionTest.php b/test/AbstractCreateConnectionTest.php new file mode 100644 index 0000000..e8d85a6 --- /dev/null +++ b/test/AbstractCreateConnectionTest.php @@ -0,0 +1,63 @@ +connect( + PostgresConfig::fromString('host=localhost user=postgres password=postgres'), + new TimeoutCancellation(1) + ); + + $this->assertFalse($connection->isClosed()); + + $connection->onClose($this->createCallback(1)); + $connection->close(); + + $this->assertTrue($connection->isClosed()); + } + + /** + * @depends testConnect + */ + public function testConnectCancellationBeforeConnect() + { + $this->expectException(CancelledException::class); + + $source = new DeferredCancellation; + $cancellation = $source->getCancellation(); + $source->cancel(); + $this->connect(PostgresConfig::fromString('host=localhost user=postgres password=postgres'), $cancellation); + } + + /** + * @depends testConnectCancellationBeforeConnect + */ + public function testConnectCancellationAfterConnect() + { + $source = new DeferredCancellation; + $cancellation = $source->getCancellation(); + $connection = $this->connect(PostgresConfig::fromString('host=localhost user=postgres password=postgres'), $cancellation); + $source->cancel(); + + $this->assertFalse($connection->isClosed()); + } + + /** + * @depends testConnectCancellationBeforeConnect + */ + public function testConnectInvalidUser() + { + $this->expectException(SqlException::class); + + $this->connect(PostgresConfig::fromString('host=localhost user=invalid password=invalid'), new TimeoutCancellation(100)); + } +} diff --git a/test/AbstractLinkTest.php b/test/AbstractLinkTest.php index 53227db..295d9d2 100644 --- a/test/AbstractLinkTest.php +++ b/test/AbstractLinkTest.php @@ -4,6 +4,7 @@ use Amp\Future; use Amp\PHPUnit\AsyncTestCase; +use Amp\Postgres\ByteA; use Amp\Postgres\PostgresConnection; use Amp\Postgres\PostgresLink; use Amp\Postgres\PostgresListener; @@ -26,24 +27,35 @@ abstract class AbstractLinkTest extends AsyncTestCase enabled BOOLEAN NOT NULL, number DOUBLE PRECISION NOT NULL, nullable CHAR(1) DEFAULT NULL, + bytea BYTEA DEFAULT NULL, PRIMARY KEY (domain, tld))"; protected const DROP_QUERY = "DROP TABLE IF EXISTS test"; - protected const INSERT_QUERY = 'INSERT INTO test VALUES ($1, $2, $3, $4, $5, $6)'; - protected const FIELD_COUNT = 6; + protected const INSERT_QUERY = 'INSERT INTO test VALUES ($1, $2, $3, $4, $5, $6, $7)'; + protected const FIELD_COUNT = 7; protected PostgresLink $link; + private ?array $data = null; + + protected function getParams(): array + { + return $this->data ??= [ + ['amphp', 'org', [1], true, 3.14159, null, new ByteA(\random_bytes(10))], + ['github', 'com', [1, 2, 3, 4, 5], false, 2.71828, null, new ByteA(\str_repeat("\0", 10))], + ['google', 'com', [1, 2, 3, 4], true, 1.61803, null, new ByteA(\random_bytes(42))], + ['php', 'net', [1, 2], false, 0.0, null, null], + ]; + } + /** * @return array Start test data for database. */ - public function getData(): array + protected function getData(): array { - return [ - ['amphp', 'org', [1], true, 3.14159, null], - ['github', 'com', [1, 2, 3, 4, 5], false, 2.71828, null], - ['google', 'com', [1, 2, 3, 4], true, 1.61803, null], - ['php', 'net', [1, 2], false, 0.0, null], - ]; + return \array_map(fn (array $params) => \array_map( + fn (mixed $param) => $param instanceof ByteA ? $param->getData() : $param, + $params, + ), $this->getParams()); } protected function verifyResult(Result $result, array $data): void @@ -635,4 +647,60 @@ public function testQueryAfterErroredQuery() $this->assertSame(1, $result->getRowCount()); } + + public function provideInsertParameters(): iterable { + $data = \str_repeat("\0", 10); + + yield [ + "INSERT INTO test + (domain, tld, keys, enabled, number, bytea) + VALUES (:domain, :tld, :keys, :enabled, :number, :bytea)", + "SELECT bytea FROM test WHERE domain = :domain", + [ + 'domain' => 'gitlab', + 'tld' => 'com', + 'keys' => [1, 2, 3], + 'enabled' => false, + 'number' => 2.718, + 'bytea' => new ByteA($data), + ], + ['bytea' => $data], + ]; + } + + /** + * @dataProvider provideInsertParameters + */ + public function testStatementInsertByteA( + string $insertSql, + string $selectSql, + array $params, + array $expected + ): void { + $statement = $this->link->prepare($insertSql); + + $result = $statement->execute($params); + + $this->assertSame(1, $result->getRowCount()); + + $result = $this->link->execute($selectSql, $params); + $this->assertSame($expected, $result->fetchRow()); + } + + /** + * @dataProvider provideInsertParameters + */ + public function testExecuteInsertByteA( + string $insertSql, + string $selectSql, + array $params, + array $expected + ): void { + $result = $this->link->execute($insertSql, $params); + + $this->assertSame(1, $result->getRowCount()); + + $result = $this->link->execute($selectSql, $params); + $this->assertSame($expected, $result->fetchRow()); + } } diff --git a/test/AbstractQuoteTest.php b/test/AbstractQuoteTest.php new file mode 100644 index 0000000..4c4d420 --- /dev/null +++ b/test/AbstractQuoteTest.php @@ -0,0 +1,38 @@ +connection = $this->connect(PostgresConfig::fromString('host=localhost user=postgres password=postgres')); + } + + public function tearDown(): void + { + parent::tearDown(); + $this->connection->close(); + } + + public function testEscapeByteA(): void + { + $this->assertSame('\x00', $this->connection->escapeByteA("\0")); + } + + public function testQuoteString(): void + { + $this->assertSame("'\"''test''\"'", $this->connection->quoteString("\"'test'\"")); + } + + public function testQuoteName(): void + { + $this->assertSame("\"\"\"'test'\"\"\"", $this->connection->quoteName("\"'test'\"")); + } +} diff --git a/test/PgSqlConnectionTest.php b/test/PgSqlConnectionTest.php index 3288034..9622be2 100644 --- a/test/PgSqlConnectionTest.php +++ b/test/PgSqlConnectionTest.php @@ -2,6 +2,7 @@ namespace Amp\Postgres\Test; +use Amp\Postgres\ByteA; use Amp\Postgres\PgSqlConnection; use Amp\Postgres\PostgresLink; use Revolt\EventLoop; @@ -12,7 +13,7 @@ */ class PgSqlConnectionTest extends AbstractConnectionTest { - /** @var resource PostgreSQL connection resource. */ + /** @var \PgSql\Connection PostgreSQL connection resource. */ protected $handle; public function createLink(string $connectionString): PostgresLink @@ -32,19 +33,24 @@ public function createLink(string $connectionString): PostgresLink $this->fail('Could not create test table.'); } - foreach ($this->getData() as $row) { - $result = \pg_query_params($this->handle, self::INSERT_QUERY, \array_map(cast(...), $row)); + foreach ($this->getParams() as $row) { + $result = \pg_query_params($this->handle, self::INSERT_QUERY, \array_map($this->cast(...), $row)); if (!$result) { $this->fail('Could not insert test data.'); } } - return $this->newConnection(PgsqlConnection::class, $this->handle, $socket, 'mock-connection'); + return $this->newConnection(PgSqlConnection::class, $this->handle, $socket, 'mock-connection'); + } + + private function cast(mixed $param): mixed + { + return $param instanceof ByteA ? \pg_escape_bytea($this->handle, $param->getData()) : cast($param); } public function tearDown(): void { - \pg_cancel_query($this->handle); // Cancel any outstanding query. + \pg_cancel_query($this->handle); // Cancel any outstanding query. \pg_get_result($this->handle); // Consume any leftover results from test. \pg_query($this->handle, "ROLLBACK"); \pg_query($this->handle, self::DROP_QUERY); diff --git a/test/PgSqlConnectTest.php b/test/PgSqlCreateConnectionTest.php similarity index 71% rename from test/PgSqlConnectTest.php rename to test/PgSqlCreateConnectionTest.php index f717b14..2030ad6 100644 --- a/test/PgSqlConnectTest.php +++ b/test/PgSqlCreateConnectionTest.php @@ -10,9 +10,9 @@ /** * @requires extension pgsql */ -class PgSqlConnectTest extends AbstractConnectTest +class PgSqlCreateConnectionTest extends AbstractCreateConnectionTest { - public function connect(PostgresConfig $connectionConfig, Cancellation $cancellation = null): PgSqlConnection + public function connect(PostgresConfig $connectionConfig, ?Cancellation $cancellation = null): PgSqlConnection { if (EventLoop::getDriver()->getHandle() instanceof \EvLoop) { $this->markTestSkipped("ext-pgsql is not compatible with pecl-ev"); diff --git a/test/PgSqlPoolTest.php b/test/PgSqlPoolTest.php index baa02dc..6cdd796 100644 --- a/test/PgSqlPoolTest.php +++ b/test/PgSqlPoolTest.php @@ -2,6 +2,7 @@ namespace Amp\Postgres\Test; +use Amp\Postgres\ByteA; use Amp\Postgres\PgSqlConnection; use Amp\Postgres\PostgresConfig; use Amp\Postgres\PostgresConnectionPool; @@ -18,7 +19,7 @@ class PgSqlPoolTest extends AbstractLinkTest { const POOL_SIZE = 3; - /** @var resource[] PostgreSQL connection resources. */ + /** @var \PgSql\Connection[] PostgreSQL connection resources. */ protected array $handles = []; public function createLink(string $connectionString): PostgresLink @@ -33,15 +34,15 @@ public function createLink(string $connectionString): PostgresLink $connector = $this->createMock(SqlConnector::class); $connector->method('connect') - ->will($this->returnCallback(function (): PgSqlConnection { + ->willReturnCallback(function (): PgSqlConnection { static $count = 0; if (!isset($this->handles[$count])) { $this->fail("createConnection called too many times"); } $handle = $this->handles[$count]; ++$count; - return $this->newConnection(PgsqlConnection::class, $handle, \pg_socket($handle), 'mock-connection'); - })); + return $this->newConnection(PgSqlConnection::class, $handle, \pg_socket($handle), 'mock-connection'); + }); $pool = new PostgresConnectionPool(new PostgresConfig('localhost'), \count($this->handles), ConnectionPool::DEFAULT_IDLE_TIMEOUT, true, $connector); @@ -55,8 +56,8 @@ public function createLink(string $connectionString): PostgresLink $this->fail('Could not create test table.'); } - foreach ($this->getData() as $row) { - $result = \pg_query_params($handle, self::INSERT_QUERY, \array_map(cast(...), $row)); + foreach ($this->getParams() as $row) { + $result = \pg_query_params($handle, self::INSERT_QUERY, \array_map(fn ($data) => $this->cast($handle, $data), $row)); if (!$result) { $this->fail('Could not insert test data.'); @@ -66,6 +67,11 @@ public function createLink(string $connectionString): PostgresLink return $pool; } + private function cast(\PgSql\Connection $handle, mixed $param): mixed + { + return $param instanceof ByteA ? \pg_escape_bytea($handle, $param->getData()) : cast($param); + } + public function tearDown(): void { foreach ($this->handles as $handle) { diff --git a/test/PgSqlQuoteTest.php b/test/PgSqlQuoteTest.php new file mode 100644 index 0000000..f47b806 --- /dev/null +++ b/test/PgSqlQuoteTest.php @@ -0,0 +1,20 @@ +getHandle() instanceof \EvLoop) { + $this->markTestSkipped("ext-pgsql is not compatible with pecl-ev"); + } + + return PgSqlConnection::connect($connectionConfig, $cancellation); + } +} diff --git a/test/PqConnectionTest.php b/test/PqConnectionTest.php index d353a7f..d0bc5d9 100644 --- a/test/PqConnectionTest.php +++ b/test/PqConnectionTest.php @@ -2,6 +2,7 @@ namespace Amp\Postgres\Test; +use Amp\Postgres\ByteA; use Amp\Postgres\Internal\PqBufferedResultSet; use Amp\Postgres\Internal\PqUnbufferedResultSet; use Amp\Postgres\PostgresLink; @@ -30,8 +31,8 @@ public function createLink(string $connectionString): PostgresLink $this->fail('Could not create test table.'); } - foreach ($this->getData() as $row) { - $result = $this->handle->execParams(self::INSERT_QUERY, \array_map(cast(...), $row)); + foreach ($this->getParams() as $row) { + $result = $this->handle->execParams(self::INSERT_QUERY, \array_map($this->cast(...), $row)); if (!$result) { $this->fail('Could not insert test data.'); @@ -41,9 +42,14 @@ public function createLink(string $connectionString): PostgresLink return $this->newConnection(PqConnection::class, $this->handle); } + private function cast(mixed $param): mixed + { + return $param instanceof ByteA ? $this->handle->escapeBytea($param->getData()) : cast($param); + } + public function tearDown(): void { - $this->handle->reset(); + $this->handle->reset(); $this->handle->exec("ROLLBACK"); $this->handle->exec(self::DROP_QUERY); diff --git a/test/PqCreateConnectionTest.php b/test/PqCreateConnectionTest.php new file mode 100644 index 0000000..e73d9b6 --- /dev/null +++ b/test/PqCreateConnectionTest.php @@ -0,0 +1,18 @@ +createMock(SqlConnector::class); $connector->method('connect') - ->will($this->returnCallback(function (): PqConnection { + ->willReturnCallback(function (): PqConnection { static $count = 0; if (!isset($this->handles[$count])) { $this->fail("createConnection called too many times"); @@ -38,7 +39,7 @@ public function createLink(string $connectionString): PostgresLink $handle = $this->handles[$count]; ++$count; return $this->newConnection(PqConnection::class, $handle); - })); + }); $pool = new PostgresConnectionPool(new PostgresConfig('localhost'), \count($this->handles), ConnectionPool::DEFAULT_IDLE_TIMEOUT, true, $connector); @@ -52,8 +53,8 @@ public function createLink(string $connectionString): PostgresLink $this->fail('Could not create test table.'); } - foreach ($this->getData() as $row) { - $result = $handle->execParams(self::INSERT_QUERY, \array_map(cast(...), $row)); + foreach ($this->getParams() as $row) { + $result = $handle->execParams(self::INSERT_QUERY, \array_map(fn ($data) => $this->cast($handle, $data), $row)); if (!$result) { $this->fail('Could not insert test data.'); @@ -63,6 +64,11 @@ public function createLink(string $connectionString): PostgresLink return $pool; } + private function cast(\pq\Connection $connection, mixed $param): mixed + { + return $param instanceof ByteA ? $connection->escapeBytea($param->getData()) : cast($param); + } + public function tearDown(): void { $this->handles[0]->exec("ROLLBACK"); diff --git a/test/PqConnectTest.php b/test/PqQuoteTest.php similarity index 63% rename from test/PqConnectTest.php rename to test/PqQuoteTest.php index 5b8f2ea..8f8ca21 100644 --- a/test/PqConnectTest.php +++ b/test/PqQuoteTest.php @@ -9,9 +9,9 @@ /** * @requires extension pq */ -class PqConnectTest extends AbstractConnectTest +class PqQuoteTest extends AbstractQuoteTest { - public function connect(PostgresConfig $connectionConfig, Cancellation $cancellation = null): PqConnection + public function connect(PostgresConfig $connectionConfig, ?Cancellation $cancellation = null): PqConnection { return PqConnection::connect($connectionConfig, $cancellation); }