Skip to content

Commit

Permalink
Improve code for encoding statement parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
trowski committed Jan 4, 2025
1 parent c62555c commit 0d1d0b3
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 88 deletions.
9 changes: 2 additions & 7 deletions src/Internal/AbstractHandle.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use Amp\ForbidCloning;
use Amp\ForbidSerialization;
use Amp\Pipeline\Queue;
use Amp\Postgres\PostgresByteA;
use Amp\Postgres\PostgresConfig;
use Amp\Sql\SqlConnectionException;
use Revolt\EventLoop;
Expand Down Expand Up @@ -90,13 +89,9 @@ protected static function shutdown(
}
}

protected function escapeParams(array $params): array
protected function encodeParam(mixed $value): string|int|float|null
{
return \array_map(fn (mixed $param) => match (true) {
$param instanceof PostgresByteA => $this->escapeByteA($param->getData()),
\is_array($param) => $this->escapeParams($param),
default => $param,
}, $params);
return encodeParam($this, $value);
}

public function commit(): void
Expand Down
4 changes: 2 additions & 2 deletions src/Internal/PgSqlHandle.php
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ 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");
$result = $this->send(\pg_send_execute(...), $name, \array_map(cast(...), $this->escapeParams($params)));
$result = $this->send(\pg_send_execute(...), $name, \array_map($this->encodeParam(...), $params));
return $this->createResult($result, $this->statements[$name]->sql);
}

Expand Down Expand Up @@ -444,7 +444,7 @@ public function execute(string $sql, array $params = []): PostgresResult
$result = $this->send(
\pg_send_query_params(...),
$sql,
\array_map(cast(...), $this->escapeParams($params))
\array_map($this->encodeParam(...), $params)
);

return $this->createResult($result, $sql);
Expand Down
4 changes: 2 additions & 2 deletions src/Internal/PgSqlResultIterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ private function cast(int $oid, ?string $value): array|bool|int|float|string|nul
$type->delimiter,
),
},
'B' => match ($value) {
'B' => match ($value) { // Boolean
't' => true,
'f' => false,
default => throw new PostgresParseException('Unexpected value for boolean field: ' . $value),
}, // Boolean
},
'N' => match ($type->name) { // Numeric
'float4', 'float8' => (float) $value,
'int2', 'int4', 'oid' => (int) $value,
Expand Down
4 changes: 2 additions & 2 deletions src/Internal/PqHandle.php
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ public function statementExecute(string $name, array $params): PostgresResult
return $this->send(
$storage->sql,
$statement->execAsync(...),
\array_map(cast(...), $this->escapeParams($params)),
\array_map($this->encodeParam(...), $params),
);
}

Expand Down Expand Up @@ -367,7 +367,7 @@ public function execute(string $sql, array $params = []): PostgresResult
$sql,
$this->handle->execParamsAsync(...),
$sql,
\array_map(cast(...), $this->escapeParams($params)),
\array_map($this->encodeParam(...), $params),
);
}

Expand Down
31 changes: 15 additions & 16 deletions src/Internal/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Amp\Postgres\Internal;

use Amp\Postgres\PostgresByteA;
use Amp\Postgres\PostgresExecutor;

/** @internal */
const STATEMENT_PARAM_REGEX = <<<'REGEX'
[
Expand Down Expand Up @@ -96,21 +99,20 @@ function replaceNamedParams(array $params, array $names): array
* @internal
*
* Casts a PHP value to a representation that is understood by Postgres, including encoding arrays.
*
* @throws \Error If $value is an object which is not a BackedEnum or Stringable, a resource, or an unknown type.
*/
function cast(mixed $value): string|int|float|null
function encodeParam(PostgresExecutor $executor, mixed $value): string|int|float|null
{
return match (\gettype($value)) {
"NULL", "integer", "double", "string" => $value,
"boolean" => $value ? 't' : 'f',
"array" => '{' . \implode(',', \array_map(encodeArrayItem(...), $value)) . '}',
"array" => '{' . \implode(',', \array_map(fn ($i) => encodeArrayItem($executor, $i), $value)) . '}',
"object" => match (true) {
$value instanceof PostgresByteA => $executor->escapeByteA($value->getData()),
$value instanceof \BackedEnum => $value->value,
$value instanceof \Stringable => (string) $value,
default => throw new \TypeError(
"An object in parameter values must be a BackedEnum or implement Stringable; got instance of "
. \get_debug_type($value)
"An object in parameter values must be a PostgresByteA, a BackedEnum, or implement Stringable; "
. "got instance of " . \get_debug_type($value)
),
},
default => throw new \TypeError(\sprintf(
Expand All @@ -125,19 +127,16 @@ function cast(mixed $value): string|int|float|null
*
* Wraps string in double-quotes for inclusion in an array.
*/
function encodeArrayItem(mixed $value): mixed
function encodeArrayItem(PostgresExecutor $executor, mixed $value): mixed
{
return match (\gettype($value)) {
"NULL" => "NULL",
"string" => '"' . \str_replace(['\\', '"'], ['\\\\', '\\"'], $value) . '"',
"object" => match (true) {
$value instanceof \BackedEnum => encodeArrayItem($value->value),
$value instanceof \Stringable => encodeArrayItem((string) $value),
default => throw new \TypeError(
"An object in parameter arrays must be a BackedEnum or implement Stringable; "
. "got instance of " . \get_debug_type($value)
),
},
default => cast($value),
"array", "boolean", "integer", "double" => encodeParam($executor, $value),
"object" => encodeArrayItem($executor, encodeParam($executor, $value)),
default => throw new \TypeError(\sprintf(
"Invalid value type '%s' in array",
\get_debug_type($value),
)),
};
}
8 changes: 8 additions & 0 deletions test/AbstractLinkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Amp\Sql\SqlStatement;
use Amp\Sql\SqlTransactionError;
use function Amp\async;
use function Amp\Postgres\Internal\encodeParam;

abstract class AbstractLinkTest extends AsyncTestCase
{
Expand Down Expand Up @@ -73,6 +74,13 @@ protected function verifyResult(SqlResult $result, array $data): void
}
}

protected function encodeParam(PostgresExecutor $executor, mixed $value): string|int|null|float
{
return $value instanceof PostgresByteA
? $executor->escapeByteA($value->getData())
: encodeParam($executor, $value);
}

/**
* @return PostgresLink Executor object to be tested.
*/
Expand Down
46 changes: 26 additions & 20 deletions test/CastTest.php → test/EncodeParamTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

namespace Amp\Postgres\Test;

use Amp\Postgres\PostgresExecutor;
use PHPUnit\Framework\TestCase;
use function Amp\Postgres\Internal\cast;
use function Amp\Postgres\Internal\encodeParam;

enum IntegerEnum: int
{
Expand All @@ -24,84 +25,89 @@ enum UnitEnum
case Case;
}

class CastTest extends TestCase
class EncodeParamTest extends TestCase
{
private function encodeParam(mixed $param): string|int|float|null
{
return encodeParam($this->createMock(PostgresExecutor::class), $param);
}

public function testSingleDimensionalStringArray(): void
{
$array = ["one", "two", "three"];
$string = '{"one","two","three"}';

$this->assertSame($string, cast($array));
$this->assertSame($string, $this->encodeParam($array));
}

public function testMultiDimensionalStringArray(): void
{
$array = ["one", "two", ["three", "four"], "five"];
$string = '{"one","two",{"three","four"},"five"}';

$this->assertSame($string, cast($array));
$this->assertSame($string, $this->encodeParam($array));
}

public function testQuotedStrings(): void
{
$array = ["one", "two", ["three", "four"], "five"];
$string = '{"one","two",{"three","four"},"five"}';

$this->assertSame($string, cast($array));
$this->assertSame($string, $this->encodeParam($array));
}

public function testEscapedQuoteDelimiter(): void
{
$array = ['va"lue1', 'value"2'];
$string = '{"va\\"lue1","value\\"2"}';

$this->assertSame($string, cast($array));
$this->assertSame($string, $this->encodeParam($array));
}

public function testNullValue(): void
{
$array = ["one", null, "three"];
$string = '{"one",NULL,"three"}';

$this->assertSame($string, cast($array));
$this->assertSame($string, $this->encodeParam($array));
}

public function testSingleDimensionalIntegerArray(): void
{
$array = [1, 2, 3];
$string = '{' . \implode(',', $array) . '}';

$this->assertSame($string, cast($array));
$this->assertSame($string, $this->encodeParam($array));
}

public function testIntegerArrayWithNull(): void
{
$array = [1, 2, null, 3];
$string = '{1,2,NULL,3}';

$this->assertSame($string, cast($array));
$this->assertSame($string, $this->encodeParam($array));
}

public function testMultidimensionalIntegerArray(): void
{
$array = [1, 2, [3, 4], [5], 6, 7, [[8, 9], 10]];
$string = '{1,2,{3,4},{5},6,7,{{8,9},10}}';

$this->assertSame($string, cast($array));
$this->assertSame($string, $this->encodeParam($array));
}

public function testEscapedBackslashesInQuotedValue(): void
{
$array = ["test\\ing", "esca\\ped\\"];
$string = '{"test\\\\ing","esca\\\\ped\\\\"}';

$this->assertSame($string, cast($array));
$this->assertSame($string, $this->encodeParam($array));
}

public function testBackedEnum(): void
{
$this->assertSame(3, cast(IntegerEnum::Three));
$this->assertSame('three', cast(StringEnum::Three));
$this->assertSame(3, $this->encodeParam(IntegerEnum::Three));
$this->assertSame('three', $this->encodeParam(StringEnum::Three));
}

public function testBackedEnumInArray(): void
Expand All @@ -112,38 +118,38 @@ public function testBackedEnumInArray(): void
];
$string = '{{1,2,3},{"one","two","three"}}';

$this->assertSame($string, cast($array));
$this->assertSame($string, $this->encodeParam($array));
}

public function testUnitEnum(): void
{
$this->expectException(\TypeError::class);
$this->expectExceptionMessage('An object in parameter values must be');

cast(UnitEnum::Case);
$this->encodeParam(UnitEnum::Case);
}

public function testUnitEnumInArray(): void
{
$this->expectException(\TypeError::class);
$this->expectExceptionMessage('An object in parameter arrays must be');
$this->expectExceptionMessage('An object in parameter values must be');

cast([UnitEnum::Case]);
$this->encodeParam([UnitEnum::Case]);
}

public function testObjectWithoutToStringMethod(): void
{
$this->expectException(\TypeError::class);
$this->expectExceptionMessage('An object in parameter values must be');

cast(new \stdClass);
$this->encodeParam(new \stdClass);
}

public function testObjectWithoutToStringMethodInArray(): void
{
$this->expectException(\TypeError::class);
$this->expectExceptionMessage('An object in parameter arrays must be');
$this->expectExceptionMessage('An object in parameter values must be');

cast([new \stdClass]);
$this->encodeParam([new \stdClass]);
}
}
28 changes: 14 additions & 14 deletions test/PgSqlConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
namespace Amp\Postgres\Test;

use Amp\Postgres\PgSqlConnection;
use Amp\Postgres\PostgresByteA;
use Amp\Postgres\PostgresConfig;
use Amp\Postgres\PostgresLink;
use Revolt\EventLoop;
use function Amp\Postgres\Internal\cast;

/**
* @requires extension pgsql
Expand All @@ -33,25 +31,27 @@ public function createLink(string $connectionString): PostgresLink
$this->fail('Could not create test table.');
}

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(
$connection = $this->newConnection(
PgSqlConnection::class,
$this->handle,
$socket,
'mock-connection',
PostgresConfig::fromString($connectionString),
);
}

private function cast(mixed $param): mixed
{
return $param instanceof PostgresByteA ? \pg_escape_bytea($this->handle, $param->getData()) : cast($param);
foreach ($this->getParams() as $row) {
$result = \pg_query_params(
$this->handle,
self::INSERT_QUERY,
\array_map(fn ($data) => $this->encodeParam($connection, $data), $row),
);

if (!$result) {
$this->fail('Could not insert test data.');
}
}

return $connection;
}

public function tearDown(): void
Expand Down
Loading

0 comments on commit 0d1d0b3

Please sign in to comment.