Skip to content

Commit 0d1d0b3

Browse files
committed
Improve code for encoding statement parameters
1 parent c62555c commit 0d1d0b3

11 files changed

+105
-88
lines changed

src/Internal/AbstractHandle.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Amp\ForbidCloning;
77
use Amp\ForbidSerialization;
88
use Amp\Pipeline\Queue;
9-
use Amp\Postgres\PostgresByteA;
109
use Amp\Postgres\PostgresConfig;
1110
use Amp\Sql\SqlConnectionException;
1211
use Revolt\EventLoop;
@@ -90,13 +89,9 @@ protected static function shutdown(
9089
}
9190
}
9291

93-
protected function escapeParams(array $params): array
92+
protected function encodeParam(mixed $value): string|int|float|null
9493
{
95-
return \array_map(fn (mixed $param) => match (true) {
96-
$param instanceof PostgresByteA => $this->escapeByteA($param->getData()),
97-
\is_array($param) => $this->escapeParams($param),
98-
default => $param,
99-
}, $params);
94+
return encodeParam($this, $value);
10095
}
10196

10297
public function commit(): void

src/Internal/PgSqlHandle.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ private function fetchNextResult(string $sql): ?PostgresResult
381381
public function statementExecute(string $name, array $params): PostgresResult
382382
{
383383
\assert(isset($this->statements[$name]), "Named statement not found when executing");
384-
$result = $this->send(\pg_send_execute(...), $name, \array_map(cast(...), $this->escapeParams($params)));
384+
$result = $this->send(\pg_send_execute(...), $name, \array_map($this->encodeParam(...), $params));
385385
return $this->createResult($result, $this->statements[$name]->sql);
386386
}
387387

@@ -444,7 +444,7 @@ public function execute(string $sql, array $params = []): PostgresResult
444444
$result = $this->send(
445445
\pg_send_query_params(...),
446446
$sql,
447-
\array_map(cast(...), $this->escapeParams($params))
447+
\array_map($this->encodeParam(...), $params)
448448
);
449449

450450
return $this->createResult($result, $sql);

src/Internal/PgSqlResultIterator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,11 @@ private function cast(int $oid, ?string $value): array|bool|int|float|string|nul
9090
$type->delimiter,
9191
),
9292
},
93-
'B' => match ($value) {
93+
'B' => match ($value) { // Boolean
9494
't' => true,
9595
'f' => false,
9696
default => throw new PostgresParseException('Unexpected value for boolean field: ' . $value),
97-
}, // Boolean
97+
},
9898
'N' => match ($type->name) { // Numeric
9999
'float4', 'float8' => (float) $value,
100100
'int2', 'int4', 'oid' => (int) $value,

src/Internal/PqHandle.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ public function statementExecute(string $name, array $params): PostgresResult
303303
return $this->send(
304304
$storage->sql,
305305
$statement->execAsync(...),
306-
\array_map(cast(...), $this->escapeParams($params)),
306+
\array_map($this->encodeParam(...), $params),
307307
);
308308
}
309309

@@ -367,7 +367,7 @@ public function execute(string $sql, array $params = []): PostgresResult
367367
$sql,
368368
$this->handle->execParamsAsync(...),
369369
$sql,
370-
\array_map(cast(...), $this->escapeParams($params)),
370+
\array_map($this->encodeParam(...), $params),
371371
);
372372
}
373373

src/Internal/functions.php

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace Amp\Postgres\Internal;
44

5+
use Amp\Postgres\PostgresByteA;
6+
use Amp\Postgres\PostgresExecutor;
7+
58
/** @internal */
69
const STATEMENT_PARAM_REGEX = <<<'REGEX'
710
[
@@ -96,21 +99,20 @@ function replaceNamedParams(array $params, array $names): array
9699
* @internal
97100
*
98101
* Casts a PHP value to a representation that is understood by Postgres, including encoding arrays.
99-
*
100-
* @throws \Error If $value is an object which is not a BackedEnum or Stringable, a resource, or an unknown type.
101102
*/
102-
function cast(mixed $value): string|int|float|null
103+
function encodeParam(PostgresExecutor $executor, mixed $value): string|int|float|null
103104
{
104105
return match (\gettype($value)) {
105106
"NULL", "integer", "double", "string" => $value,
106107
"boolean" => $value ? 't' : 'f',
107-
"array" => '{' . \implode(',', \array_map(encodeArrayItem(...), $value)) . '}',
108+
"array" => '{' . \implode(',', \array_map(fn ($i) => encodeArrayItem($executor, $i), $value)) . '}',
108109
"object" => match (true) {
110+
$value instanceof PostgresByteA => $executor->escapeByteA($value->getData()),
109111
$value instanceof \BackedEnum => $value->value,
110112
$value instanceof \Stringable => (string) $value,
111113
default => throw new \TypeError(
112-
"An object in parameter values must be a BackedEnum or implement Stringable; got instance of "
113-
. \get_debug_type($value)
114+
"An object in parameter values must be a PostgresByteA, a BackedEnum, or implement Stringable; "
115+
. "got instance of " . \get_debug_type($value)
114116
),
115117
},
116118
default => throw new \TypeError(\sprintf(
@@ -125,19 +127,16 @@ function cast(mixed $value): string|int|float|null
125127
*
126128
* Wraps string in double-quotes for inclusion in an array.
127129
*/
128-
function encodeArrayItem(mixed $value): mixed
130+
function encodeArrayItem(PostgresExecutor $executor, mixed $value): mixed
129131
{
130132
return match (\gettype($value)) {
131133
"NULL" => "NULL",
132134
"string" => '"' . \str_replace(['\\', '"'], ['\\\\', '\\"'], $value) . '"',
133-
"object" => match (true) {
134-
$value instanceof \BackedEnum => encodeArrayItem($value->value),
135-
$value instanceof \Stringable => encodeArrayItem((string) $value),
136-
default => throw new \TypeError(
137-
"An object in parameter arrays must be a BackedEnum or implement Stringable; "
138-
. "got instance of " . \get_debug_type($value)
139-
),
140-
},
141-
default => cast($value),
135+
"array", "boolean", "integer", "double" => encodeParam($executor, $value),
136+
"object" => encodeArrayItem($executor, encodeParam($executor, $value)),
137+
default => throw new \TypeError(\sprintf(
138+
"Invalid value type '%s' in array",
139+
\get_debug_type($value),
140+
)),
142141
};
143142
}

test/AbstractLinkTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Amp\Sql\SqlStatement;
1616
use Amp\Sql\SqlTransactionError;
1717
use function Amp\async;
18+
use function Amp\Postgres\Internal\encodeParam;
1819

1920
abstract class AbstractLinkTest extends AsyncTestCase
2021
{
@@ -73,6 +74,13 @@ protected function verifyResult(SqlResult $result, array $data): void
7374
}
7475
}
7576

77+
protected function encodeParam(PostgresExecutor $executor, mixed $value): string|int|null|float
78+
{
79+
return $value instanceof PostgresByteA
80+
? $executor->escapeByteA($value->getData())
81+
: encodeParam($executor, $value);
82+
}
83+
7684
/**
7785
* @return PostgresLink Executor object to be tested.
7886
*/

test/CastTest.php renamed to test/EncodeParamTest.php

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
namespace Amp\Postgres\Test;
44

5+
use Amp\Postgres\PostgresExecutor;
56
use PHPUnit\Framework\TestCase;
6-
use function Amp\Postgres\Internal\cast;
7+
use function Amp\Postgres\Internal\encodeParam;
78

89
enum IntegerEnum: int
910
{
@@ -24,84 +25,89 @@ enum UnitEnum
2425
case Case;
2526
}
2627

27-
class CastTest extends TestCase
28+
class EncodeParamTest extends TestCase
2829
{
30+
private function encodeParam(mixed $param): string|int|float|null
31+
{
32+
return encodeParam($this->createMock(PostgresExecutor::class), $param);
33+
}
34+
2935
public function testSingleDimensionalStringArray(): void
3036
{
3137
$array = ["one", "two", "three"];
3238
$string = '{"one","two","three"}';
3339

34-
$this->assertSame($string, cast($array));
40+
$this->assertSame($string, $this->encodeParam($array));
3541
}
3642

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

42-
$this->assertSame($string, cast($array));
48+
$this->assertSame($string, $this->encodeParam($array));
4349
}
4450

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

50-
$this->assertSame($string, cast($array));
56+
$this->assertSame($string, $this->encodeParam($array));
5157
}
5258

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

58-
$this->assertSame($string, cast($array));
64+
$this->assertSame($string, $this->encodeParam($array));
5965
}
6066

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

66-
$this->assertSame($string, cast($array));
72+
$this->assertSame($string, $this->encodeParam($array));
6773
}
6874

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

74-
$this->assertSame($string, cast($array));
80+
$this->assertSame($string, $this->encodeParam($array));
7581
}
7682

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

82-
$this->assertSame($string, cast($array));
88+
$this->assertSame($string, $this->encodeParam($array));
8389
}
8490

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

90-
$this->assertSame($string, cast($array));
96+
$this->assertSame($string, $this->encodeParam($array));
9197
}
9298

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

98-
$this->assertSame($string, cast($array));
104+
$this->assertSame($string, $this->encodeParam($array));
99105
}
100106

101107
public function testBackedEnum(): void
102108
{
103-
$this->assertSame(3, cast(IntegerEnum::Three));
104-
$this->assertSame('three', cast(StringEnum::Three));
109+
$this->assertSame(3, $this->encodeParam(IntegerEnum::Three));
110+
$this->assertSame('three', $this->encodeParam(StringEnum::Three));
105111
}
106112

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

115-
$this->assertSame($string, cast($array));
121+
$this->assertSame($string, $this->encodeParam($array));
116122
}
117123

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

123-
cast(UnitEnum::Case);
129+
$this->encodeParam(UnitEnum::Case);
124130
}
125131

126132
public function testUnitEnumInArray(): void
127133
{
128134
$this->expectException(\TypeError::class);
129-
$this->expectExceptionMessage('An object in parameter arrays must be');
135+
$this->expectExceptionMessage('An object in parameter values must be');
130136

131-
cast([UnitEnum::Case]);
137+
$this->encodeParam([UnitEnum::Case]);
132138
}
133139

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

139-
cast(new \stdClass);
145+
$this->encodeParam(new \stdClass);
140146
}
141147

142148
public function testObjectWithoutToStringMethodInArray(): void
143149
{
144150
$this->expectException(\TypeError::class);
145-
$this->expectExceptionMessage('An object in parameter arrays must be');
151+
$this->expectExceptionMessage('An object in parameter values must be');
146152

147-
cast([new \stdClass]);
153+
$this->encodeParam([new \stdClass]);
148154
}
149155
}

test/PgSqlConnectionTest.php

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
namespace Amp\Postgres\Test;
44

55
use Amp\Postgres\PgSqlConnection;
6-
use Amp\Postgres\PostgresByteA;
76
use Amp\Postgres\PostgresConfig;
87
use Amp\Postgres\PostgresLink;
98
use Revolt\EventLoop;
10-
use function Amp\Postgres\Internal\cast;
119

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

36-
foreach ($this->getParams() as $row) {
37-
$result = \pg_query_params($this->handle, self::INSERT_QUERY, \array_map($this->cast(...), $row));
38-
if (!$result) {
39-
$this->fail('Could not insert test data.');
40-
}
41-
}
42-
43-
return $this->newConnection(
34+
$connection = $this->newConnection(
4435
PgSqlConnection::class,
4536
$this->handle,
4637
$socket,
4738
'mock-connection',
4839
PostgresConfig::fromString($connectionString),
4940
);
50-
}
5141

52-
private function cast(mixed $param): mixed
53-
{
54-
return $param instanceof PostgresByteA ? \pg_escape_bytea($this->handle, $param->getData()) : cast($param);
42+
foreach ($this->getParams() as $row) {
43+
$result = \pg_query_params(
44+
$this->handle,
45+
self::INSERT_QUERY,
46+
\array_map(fn ($data) => $this->encodeParam($connection, $data), $row),
47+
);
48+
49+
if (!$result) {
50+
$this->fail('Could not insert test data.');
51+
}
52+
}
53+
54+
return $connection;
5555
}
5656

5757
public function tearDown(): void

0 commit comments

Comments
 (0)