diff options
Diffstat (limited to 'vendor/doctrine/dbal/src/Driver/PgSQL')
8 files changed, 677 insertions, 0 deletions
diff --git a/vendor/doctrine/dbal/src/Driver/PgSQL/Connection.php b/vendor/doctrine/dbal/src/Driver/PgSQL/Connection.php new file mode 100644 index 0000000..4f280b0 --- /dev/null +++ b/vendor/doctrine/dbal/src/Driver/PgSQL/Connection.php | |||
@@ -0,0 +1,129 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\DBAL\Driver\PgSQL; | ||
6 | |||
7 | use Doctrine\DBAL\Driver\Connection as ConnectionInterface; | ||
8 | use Doctrine\DBAL\Driver\Exception\NoIdentityValue; | ||
9 | use Doctrine\DBAL\SQL\Parser; | ||
10 | use PgSql\Connection as PgSqlConnection; | ||
11 | |||
12 | use function assert; | ||
13 | use function pg_close; | ||
14 | use function pg_escape_literal; | ||
15 | use function pg_get_result; | ||
16 | use function pg_last_error; | ||
17 | use function pg_result_error; | ||
18 | use function pg_send_prepare; | ||
19 | use function pg_send_query; | ||
20 | use function pg_version; | ||
21 | use function uniqid; | ||
22 | |||
23 | final class Connection implements ConnectionInterface | ||
24 | { | ||
25 | private readonly Parser $parser; | ||
26 | |||
27 | public function __construct(private readonly PgSqlConnection $connection) | ||
28 | { | ||
29 | $this->parser = new Parser(false); | ||
30 | } | ||
31 | |||
32 | public function __destruct() | ||
33 | { | ||
34 | if (! isset($this->connection)) { | ||
35 | return; | ||
36 | } | ||
37 | |||
38 | @pg_close($this->connection); | ||
39 | } | ||
40 | |||
41 | public function prepare(string $sql): Statement | ||
42 | { | ||
43 | $visitor = new ConvertParameters(); | ||
44 | $this->parser->parse($sql, $visitor); | ||
45 | |||
46 | $statementName = uniqid('dbal', true); | ||
47 | if (@pg_send_prepare($this->connection, $statementName, $visitor->getSQL()) !== true) { | ||
48 | throw new Exception(pg_last_error($this->connection)); | ||
49 | } | ||
50 | |||
51 | $result = @pg_get_result($this->connection); | ||
52 | assert($result !== false); | ||
53 | |||
54 | if ((bool) pg_result_error($result)) { | ||
55 | throw Exception::fromResult($result); | ||
56 | } | ||
57 | |||
58 | return new Statement($this->connection, $statementName, $visitor->getParameterMap()); | ||
59 | } | ||
60 | |||
61 | public function query(string $sql): Result | ||
62 | { | ||
63 | if (@pg_send_query($this->connection, $sql) !== true) { | ||
64 | throw new Exception(pg_last_error($this->connection)); | ||
65 | } | ||
66 | |||
67 | $result = @pg_get_result($this->connection); | ||
68 | assert($result !== false); | ||
69 | |||
70 | if ((bool) pg_result_error($result)) { | ||
71 | throw Exception::fromResult($result); | ||
72 | } | ||
73 | |||
74 | return new Result($result); | ||
75 | } | ||
76 | |||
77 | /** {@inheritDoc} */ | ||
78 | public function quote(string $value): string | ||
79 | { | ||
80 | $quotedValue = pg_escape_literal($this->connection, $value); | ||
81 | assert($quotedValue !== false); | ||
82 | |||
83 | return $quotedValue; | ||
84 | } | ||
85 | |||
86 | public function exec(string $sql): int | ||
87 | { | ||
88 | return $this->query($sql)->rowCount(); | ||
89 | } | ||
90 | |||
91 | /** {@inheritDoc} */ | ||
92 | public function lastInsertId(): int|string | ||
93 | { | ||
94 | try { | ||
95 | return $this->query('SELECT LASTVAL()')->fetchOne(); | ||
96 | } catch (Exception $exception) { | ||
97 | if ($exception->getSQLState() === '55000') { | ||
98 | throw NoIdentityValue::new($exception); | ||
99 | } | ||
100 | |||
101 | throw $exception; | ||
102 | } | ||
103 | } | ||
104 | |||
105 | public function beginTransaction(): void | ||
106 | { | ||
107 | $this->exec('BEGIN'); | ||
108 | } | ||
109 | |||
110 | public function commit(): void | ||
111 | { | ||
112 | $this->exec('COMMIT'); | ||
113 | } | ||
114 | |||
115 | public function rollBack(): void | ||
116 | { | ||
117 | $this->exec('ROLLBACK'); | ||
118 | } | ||
119 | |||
120 | public function getServerVersion(): string | ||
121 | { | ||
122 | return (string) pg_version($this->connection)['server']; | ||
123 | } | ||
124 | |||
125 | public function getNativeConnection(): PgSqlConnection | ||
126 | { | ||
127 | return $this->connection; | ||
128 | } | ||
129 | } | ||
diff --git a/vendor/doctrine/dbal/src/Driver/PgSQL/ConvertParameters.php b/vendor/doctrine/dbal/src/Driver/PgSQL/ConvertParameters.php new file mode 100644 index 0000000..795f12d --- /dev/null +++ b/vendor/doctrine/dbal/src/Driver/PgSQL/ConvertParameters.php | |||
@@ -0,0 +1,49 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\DBAL\Driver\PgSQL; | ||
6 | |||
7 | use Doctrine\DBAL\SQL\Parser\Visitor; | ||
8 | |||
9 | use function count; | ||
10 | use function implode; | ||
11 | |||
12 | final class ConvertParameters implements Visitor | ||
13 | { | ||
14 | /** @var list<string> */ | ||
15 | private array $buffer = []; | ||
16 | |||
17 | /** @var array<array-key, int> */ | ||
18 | private array $parameterMap = []; | ||
19 | |||
20 | public function acceptPositionalParameter(string $sql): void | ||
21 | { | ||
22 | $position = count($this->parameterMap) + 1; | ||
23 | $this->parameterMap[$position] = $position; | ||
24 | $this->buffer[] = '$' . $position; | ||
25 | } | ||
26 | |||
27 | public function acceptNamedParameter(string $sql): void | ||
28 | { | ||
29 | $position = count($this->parameterMap) + 1; | ||
30 | $this->parameterMap[$sql] = $position; | ||
31 | $this->buffer[] = '$' . $position; | ||
32 | } | ||
33 | |||
34 | public function acceptOther(string $sql): void | ||
35 | { | ||
36 | $this->buffer[] = $sql; | ||
37 | } | ||
38 | |||
39 | public function getSQL(): string | ||
40 | { | ||
41 | return implode('', $this->buffer); | ||
42 | } | ||
43 | |||
44 | /** @return array<array-key, int> */ | ||
45 | public function getParameterMap(): array | ||
46 | { | ||
47 | return $this->parameterMap; | ||
48 | } | ||
49 | } | ||
diff --git a/vendor/doctrine/dbal/src/Driver/PgSQL/Driver.php b/vendor/doctrine/dbal/src/Driver/PgSQL/Driver.php new file mode 100644 index 0000000..0d5d605 --- /dev/null +++ b/vendor/doctrine/dbal/src/Driver/PgSQL/Driver.php | |||
@@ -0,0 +1,88 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\DBAL\Driver\PgSQL; | ||
6 | |||
7 | use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver; | ||
8 | use ErrorException; | ||
9 | use SensitiveParameter; | ||
10 | |||
11 | use function addslashes; | ||
12 | use function array_filter; | ||
13 | use function array_keys; | ||
14 | use function array_map; | ||
15 | use function array_slice; | ||
16 | use function array_values; | ||
17 | use function func_get_args; | ||
18 | use function implode; | ||
19 | use function pg_connect; | ||
20 | use function restore_error_handler; | ||
21 | use function set_error_handler; | ||
22 | use function sprintf; | ||
23 | |||
24 | use const PGSQL_CONNECT_FORCE_NEW; | ||
25 | |||
26 | final class Driver extends AbstractPostgreSQLDriver | ||
27 | { | ||
28 | /** {@inheritDoc} */ | ||
29 | public function connect( | ||
30 | #[SensitiveParameter] | ||
31 | array $params, | ||
32 | ): Connection { | ||
33 | set_error_handler( | ||
34 | static function (int $severity, string $message): never { | ||
35 | throw new ErrorException($message, 0, $severity, ...array_slice(func_get_args(), 2, 2)); | ||
36 | }, | ||
37 | ); | ||
38 | |||
39 | try { | ||
40 | $connection = pg_connect($this->constructConnectionString($params), PGSQL_CONNECT_FORCE_NEW); | ||
41 | } catch (ErrorException $e) { | ||
42 | throw new Exception($e->getMessage(), '08006', 0, $e); | ||
43 | } finally { | ||
44 | restore_error_handler(); | ||
45 | } | ||
46 | |||
47 | if ($connection === false) { | ||
48 | throw new Exception('Unable to connect to Postgres server.'); | ||
49 | } | ||
50 | |||
51 | $driverConnection = new Connection($connection); | ||
52 | |||
53 | if (isset($params['application_name'])) { | ||
54 | $driverConnection->exec('SET application_name = ' . $driverConnection->quote($params['application_name'])); | ||
55 | } | ||
56 | |||
57 | return $driverConnection; | ||
58 | } | ||
59 | |||
60 | /** | ||
61 | * Constructs the Postgres connection string | ||
62 | * | ||
63 | * @param array<string, mixed> $params | ||
64 | */ | ||
65 | private function constructConnectionString( | ||
66 | #[SensitiveParameter] | ||
67 | array $params, | ||
68 | ): string { | ||
69 | $components = array_filter( | ||
70 | [ | ||
71 | 'host' => $params['host'] ?? null, | ||
72 | 'port' => $params['port'] ?? null, | ||
73 | 'dbname' => $params['dbname'] ?? 'postgres', | ||
74 | 'user' => $params['user'] ?? null, | ||
75 | 'password' => $params['password'] ?? null, | ||
76 | 'sslmode' => $params['sslmode'] ?? null, | ||
77 | 'gssencmode' => $params['gssencmode'] ?? null, | ||
78 | ], | ||
79 | static fn (int|string|null $value) => $value !== '' && $value !== null, | ||
80 | ); | ||
81 | |||
82 | return implode(' ', array_map( | ||
83 | static fn (int|string $value, string $key) => sprintf("%s='%s'", $key, addslashes((string) $value)), | ||
84 | array_values($components), | ||
85 | array_keys($components), | ||
86 | )); | ||
87 | } | ||
88 | } | ||
diff --git a/vendor/doctrine/dbal/src/Driver/PgSQL/Exception.php b/vendor/doctrine/dbal/src/Driver/PgSQL/Exception.php new file mode 100644 index 0000000..3036e55 --- /dev/null +++ b/vendor/doctrine/dbal/src/Driver/PgSQL/Exception.php | |||
@@ -0,0 +1,31 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\DBAL\Driver\PgSQL; | ||
6 | |||
7 | use Doctrine\DBAL\Driver\AbstractException; | ||
8 | use PgSql\Result as PgSqlResult; | ||
9 | |||
10 | use function pg_result_error_field; | ||
11 | |||
12 | use const PGSQL_DIAG_MESSAGE_PRIMARY; | ||
13 | use const PGSQL_DIAG_SQLSTATE; | ||
14 | |||
15 | /** | ||
16 | * @internal | ||
17 | * | ||
18 | * @psalm-immutable | ||
19 | */ | ||
20 | final class Exception extends AbstractException | ||
21 | { | ||
22 | public static function fromResult(PgSqlResult $result): self | ||
23 | { | ||
24 | $sqlstate = pg_result_error_field($result, PGSQL_DIAG_SQLSTATE); | ||
25 | if ($sqlstate === false) { | ||
26 | $sqlstate = null; | ||
27 | } | ||
28 | |||
29 | return new self((string) pg_result_error_field($result, PGSQL_DIAG_MESSAGE_PRIMARY), $sqlstate); | ||
30 | } | ||
31 | } | ||
diff --git a/vendor/doctrine/dbal/src/Driver/PgSQL/Exception/UnexpectedValue.php b/vendor/doctrine/dbal/src/Driver/PgSQL/Exception/UnexpectedValue.php new file mode 100644 index 0000000..350d80f --- /dev/null +++ b/vendor/doctrine/dbal/src/Driver/PgSQL/Exception/UnexpectedValue.php | |||
@@ -0,0 +1,29 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\DBAL\Driver\PgSQL\Exception; | ||
6 | |||
7 | use Doctrine\DBAL\Driver\Exception; | ||
8 | use UnexpectedValueException; | ||
9 | |||
10 | use function sprintf; | ||
11 | |||
12 | /** @psalm-immutable */ | ||
13 | final class UnexpectedValue extends UnexpectedValueException implements Exception | ||
14 | { | ||
15 | public static function new(string $value, string $type): self | ||
16 | { | ||
17 | return new self(sprintf( | ||
18 | 'Unexpected value "%s" of type "%s" returned by Postgres', | ||
19 | $value, | ||
20 | $type, | ||
21 | )); | ||
22 | } | ||
23 | |||
24 | /** @return null */ | ||
25 | public function getSQLState(): string|null | ||
26 | { | ||
27 | return null; | ||
28 | } | ||
29 | } | ||
diff --git a/vendor/doctrine/dbal/src/Driver/PgSQL/Exception/UnknownParameter.php b/vendor/doctrine/dbal/src/Driver/PgSQL/Exception/UnknownParameter.php new file mode 100644 index 0000000..549f96d --- /dev/null +++ b/vendor/doctrine/dbal/src/Driver/PgSQL/Exception/UnknownParameter.php | |||
@@ -0,0 +1,20 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\DBAL\Driver\PgSQL\Exception; | ||
6 | |||
7 | use Doctrine\DBAL\Driver\AbstractException; | ||
8 | |||
9 | use function sprintf; | ||
10 | |||
11 | /** @psalm-immutable */ | ||
12 | final class UnknownParameter extends AbstractException | ||
13 | { | ||
14 | public static function new(string $param): self | ||
15 | { | ||
16 | return new self( | ||
17 | sprintf('Could not find parameter %s in the SQL statement', $param), | ||
18 | ); | ||
19 | } | ||
20 | } | ||
diff --git a/vendor/doctrine/dbal/src/Driver/PgSQL/Result.php b/vendor/doctrine/dbal/src/Driver/PgSQL/Result.php new file mode 100644 index 0000000..954758c --- /dev/null +++ b/vendor/doctrine/dbal/src/Driver/PgSQL/Result.php | |||
@@ -0,0 +1,240 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\DBAL\Driver\PgSQL; | ||
6 | |||
7 | use Doctrine\DBAL\Driver\FetchUtils; | ||
8 | use Doctrine\DBAL\Driver\PgSQL\Exception\UnexpectedValue; | ||
9 | use Doctrine\DBAL\Driver\Result as ResultInterface; | ||
10 | use PgSql\Result as PgSqlResult; | ||
11 | |||
12 | use function array_keys; | ||
13 | use function array_map; | ||
14 | use function assert; | ||
15 | use function hex2bin; | ||
16 | use function pg_affected_rows; | ||
17 | use function pg_fetch_all; | ||
18 | use function pg_fetch_all_columns; | ||
19 | use function pg_fetch_assoc; | ||
20 | use function pg_fetch_row; | ||
21 | use function pg_field_name; | ||
22 | use function pg_field_type; | ||
23 | use function pg_free_result; | ||
24 | use function pg_num_fields; | ||
25 | use function substr; | ||
26 | |||
27 | use const PGSQL_ASSOC; | ||
28 | use const PGSQL_NUM; | ||
29 | use const PHP_INT_SIZE; | ||
30 | |||
31 | final class Result implements ResultInterface | ||
32 | { | ||
33 | private ?PgSqlResult $result; | ||
34 | |||
35 | public function __construct(PgSqlResult $result) | ||
36 | { | ||
37 | $this->result = $result; | ||
38 | } | ||
39 | |||
40 | public function __destruct() | ||
41 | { | ||
42 | if (! isset($this->result)) { | ||
43 | return; | ||
44 | } | ||
45 | |||
46 | $this->free(); | ||
47 | } | ||
48 | |||
49 | /** {@inheritDoc} */ | ||
50 | public function fetchNumeric(): array|false | ||
51 | { | ||
52 | if ($this->result === null) { | ||
53 | return false; | ||
54 | } | ||
55 | |||
56 | $row = pg_fetch_row($this->result); | ||
57 | if ($row === false) { | ||
58 | return false; | ||
59 | } | ||
60 | |||
61 | return $this->mapNumericRow($row, $this->fetchNumericColumnTypes()); | ||
62 | } | ||
63 | |||
64 | /** {@inheritDoc} */ | ||
65 | public function fetchAssociative(): array|false | ||
66 | { | ||
67 | if ($this->result === null) { | ||
68 | return false; | ||
69 | } | ||
70 | |||
71 | $row = pg_fetch_assoc($this->result); | ||
72 | if ($row === false) { | ||
73 | return false; | ||
74 | } | ||
75 | |||
76 | return $this->mapAssociativeRow($row, $this->fetchAssociativeColumnTypes()); | ||
77 | } | ||
78 | |||
79 | /** {@inheritDoc} */ | ||
80 | public function fetchOne(): mixed | ||
81 | { | ||
82 | return FetchUtils::fetchOne($this); | ||
83 | } | ||
84 | |||
85 | /** {@inheritDoc} */ | ||
86 | public function fetchAllNumeric(): array | ||
87 | { | ||
88 | if ($this->result === null) { | ||
89 | return []; | ||
90 | } | ||
91 | |||
92 | $types = $this->fetchNumericColumnTypes(); | ||
93 | |||
94 | return array_map( | ||
95 | fn (array $row) => $this->mapNumericRow($row, $types), | ||
96 | pg_fetch_all($this->result, PGSQL_NUM), | ||
97 | ); | ||
98 | } | ||
99 | |||
100 | /** {@inheritDoc} */ | ||
101 | public function fetchAllAssociative(): array | ||
102 | { | ||
103 | if ($this->result === null) { | ||
104 | return []; | ||
105 | } | ||
106 | |||
107 | $types = $this->fetchAssociativeColumnTypes(); | ||
108 | |||
109 | return array_map( | ||
110 | fn (array $row) => $this->mapAssociativeRow($row, $types), | ||
111 | pg_fetch_all($this->result, PGSQL_ASSOC), | ||
112 | ); | ||
113 | } | ||
114 | |||
115 | /** {@inheritDoc} */ | ||
116 | public function fetchFirstColumn(): array | ||
117 | { | ||
118 | if ($this->result === null) { | ||
119 | return []; | ||
120 | } | ||
121 | |||
122 | $postgresType = pg_field_type($this->result, 0); | ||
123 | |||
124 | return array_map( | ||
125 | fn ($value) => $this->mapType($postgresType, $value), | ||
126 | pg_fetch_all_columns($this->result), | ||
127 | ); | ||
128 | } | ||
129 | |||
130 | public function rowCount(): int | ||
131 | { | ||
132 | if ($this->result === null) { | ||
133 | return 0; | ||
134 | } | ||
135 | |||
136 | return pg_affected_rows($this->result); | ||
137 | } | ||
138 | |||
139 | public function columnCount(): int | ||
140 | { | ||
141 | if ($this->result === null) { | ||
142 | return 0; | ||
143 | } | ||
144 | |||
145 | return pg_num_fields($this->result); | ||
146 | } | ||
147 | |||
148 | public function free(): void | ||
149 | { | ||
150 | if ($this->result === null) { | ||
151 | return; | ||
152 | } | ||
153 | |||
154 | pg_free_result($this->result); | ||
155 | $this->result = null; | ||
156 | } | ||
157 | |||
158 | /** @return array<int, string> */ | ||
159 | private function fetchNumericColumnTypes(): array | ||
160 | { | ||
161 | assert($this->result !== null); | ||
162 | |||
163 | $types = []; | ||
164 | $numFields = pg_num_fields($this->result); | ||
165 | for ($i = 0; $i < $numFields; ++$i) { | ||
166 | $types[$i] = pg_field_type($this->result, $i); | ||
167 | } | ||
168 | |||
169 | return $types; | ||
170 | } | ||
171 | |||
172 | /** @return array<string, string> */ | ||
173 | private function fetchAssociativeColumnTypes(): array | ||
174 | { | ||
175 | assert($this->result !== null); | ||
176 | |||
177 | $types = []; | ||
178 | $numFields = pg_num_fields($this->result); | ||
179 | for ($i = 0; $i < $numFields; ++$i) { | ||
180 | $types[pg_field_name($this->result, $i)] = pg_field_type($this->result, $i); | ||
181 | } | ||
182 | |||
183 | return $types; | ||
184 | } | ||
185 | |||
186 | /** | ||
187 | * @param list<string|null> $row | ||
188 | * @param array<int, string> $types | ||
189 | * | ||
190 | * @return list<mixed> | ||
191 | */ | ||
192 | private function mapNumericRow(array $row, array $types): array | ||
193 | { | ||
194 | assert($this->result !== null); | ||
195 | |||
196 | return array_map( | ||
197 | fn ($value, $field) => $this->mapType($types[$field], $value), | ||
198 | $row, | ||
199 | array_keys($row), | ||
200 | ); | ||
201 | } | ||
202 | |||
203 | /** | ||
204 | * @param array<string, string|null> $row | ||
205 | * @param array<string, string> $types | ||
206 | * | ||
207 | * @return array<string, mixed> | ||
208 | */ | ||
209 | private function mapAssociativeRow(array $row, array $types): array | ||
210 | { | ||
211 | assert($this->result !== null); | ||
212 | |||
213 | $mappedRow = []; | ||
214 | foreach ($row as $field => $value) { | ||
215 | $mappedRow[$field] = $this->mapType($types[$field], $value); | ||
216 | } | ||
217 | |||
218 | return $mappedRow; | ||
219 | } | ||
220 | |||
221 | private function mapType(string $postgresType, ?string $value): string|int|float|bool|null | ||
222 | { | ||
223 | if ($value === null) { | ||
224 | return null; | ||
225 | } | ||
226 | |||
227 | return match ($postgresType) { | ||
228 | 'bool' => match ($value) { | ||
229 | 't' => true, | ||
230 | 'f' => false, | ||
231 | default => throw UnexpectedValue::new($value, $postgresType), | ||
232 | }, | ||
233 | 'bytea' => hex2bin(substr($value, 2)), | ||
234 | 'float4', 'float8' => (float) $value, | ||
235 | 'int2', 'int4' => (int) $value, | ||
236 | 'int8' => PHP_INT_SIZE >= 8 ? (int) $value : $value, | ||
237 | default => $value, | ||
238 | }; | ||
239 | } | ||
240 | } | ||
diff --git a/vendor/doctrine/dbal/src/Driver/PgSQL/Statement.php b/vendor/doctrine/dbal/src/Driver/PgSQL/Statement.php new file mode 100644 index 0000000..b48ab5d --- /dev/null +++ b/vendor/doctrine/dbal/src/Driver/PgSQL/Statement.php | |||
@@ -0,0 +1,91 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\DBAL\Driver\PgSQL; | ||
6 | |||
7 | use Doctrine\DBAL\Driver\PgSQL\Exception\UnknownParameter; | ||
8 | use Doctrine\DBAL\Driver\Statement as StatementInterface; | ||
9 | use Doctrine\DBAL\ParameterType; | ||
10 | use PgSql\Connection as PgSqlConnection; | ||
11 | |||
12 | use function assert; | ||
13 | use function is_resource; | ||
14 | use function ksort; | ||
15 | use function pg_escape_bytea; | ||
16 | use function pg_escape_identifier; | ||
17 | use function pg_get_result; | ||
18 | use function pg_last_error; | ||
19 | use function pg_query; | ||
20 | use function pg_result_error; | ||
21 | use function pg_send_execute; | ||
22 | use function stream_get_contents; | ||
23 | |||
24 | final class Statement implements StatementInterface | ||
25 | { | ||
26 | /** @var array<int, mixed> */ | ||
27 | private array $parameters = []; | ||
28 | |||
29 | /** @psalm-var array<int, ParameterType> */ | ||
30 | private array $parameterTypes = []; | ||
31 | |||
32 | /** @param array<array-key, int> $parameterMap */ | ||
33 | public function __construct( | ||
34 | private readonly PgSqlConnection $connection, | ||
35 | private readonly string $name, | ||
36 | private readonly array $parameterMap, | ||
37 | ) { | ||
38 | } | ||
39 | |||
40 | public function __destruct() | ||
41 | { | ||
42 | if (! isset($this->connection)) { | ||
43 | return; | ||
44 | } | ||
45 | |||
46 | @pg_query( | ||
47 | $this->connection, | ||
48 | 'DEALLOCATE ' . pg_escape_identifier($this->connection, $this->name), | ||
49 | ); | ||
50 | } | ||
51 | |||
52 | /** {@inheritDoc} */ | ||
53 | public function bindValue(int|string $param, mixed $value, ParameterType $type = ParameterType::STRING): void | ||
54 | { | ||
55 | if (! isset($this->parameterMap[$param])) { | ||
56 | throw UnknownParameter::new((string) $param); | ||
57 | } | ||
58 | |||
59 | $this->parameters[$this->parameterMap[$param]] = $value; | ||
60 | $this->parameterTypes[$this->parameterMap[$param]] = $type; | ||
61 | } | ||
62 | |||
63 | /** {@inheritDoc} */ | ||
64 | public function execute(): Result | ||
65 | { | ||
66 | ksort($this->parameters); | ||
67 | |||
68 | $escapedParameters = []; | ||
69 | foreach ($this->parameters as $parameter => $value) { | ||
70 | $escapedParameters[] = match ($this->parameterTypes[$parameter]) { | ||
71 | ParameterType::BINARY, ParameterType::LARGE_OBJECT => $value === null | ||
72 | ? null | ||
73 | : pg_escape_bytea($this->connection, is_resource($value) ? stream_get_contents($value) : $value), | ||
74 | default => $value, | ||
75 | }; | ||
76 | } | ||
77 | |||
78 | if (@pg_send_execute($this->connection, $this->name, $escapedParameters) !== true) { | ||
79 | throw new Exception(pg_last_error($this->connection)); | ||
80 | } | ||
81 | |||
82 | $result = @pg_get_result($this->connection); | ||
83 | assert($result !== false); | ||
84 | |||
85 | if ((bool) pg_result_error($result)) { | ||
86 | throw Exception::fromResult($result); | ||
87 | } | ||
88 | |||
89 | return new Result($result); | ||
90 | } | ||
91 | } | ||