summaryrefslogtreecommitdiff
path: root/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php')
-rw-r--r--vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php572
1 files changed, 572 insertions, 0 deletions
diff --git a/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php b/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php
new file mode 100644
index 0000000..9af16c9
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php
@@ -0,0 +1,572 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Exception;
8use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
9use Doctrine\DBAL\Result;
10use Doctrine\DBAL\Types\JsonType;
11use Doctrine\DBAL\Types\Type;
12
13use function array_change_key_case;
14use function array_key_exists;
15use function array_map;
16use function array_merge;
17use function assert;
18use function explode;
19use function implode;
20use function in_array;
21use function is_string;
22use function preg_match;
23use function sprintf;
24use function str_contains;
25use function str_replace;
26use function strtolower;
27use function trim;
28
29use const CASE_LOWER;
30
31/**
32 * PostgreSQL Schema Manager.
33 *
34 * @extends AbstractSchemaManager<PostgreSQLPlatform>
35 */
36class PostgreSQLSchemaManager extends AbstractSchemaManager
37{
38 private ?string $currentSchema = null;
39
40 /**
41 * {@inheritDoc}
42 */
43 public function listSchemaNames(): array
44 {
45 return $this->connection->fetchFirstColumn(
46 <<<'SQL'
47SELECT schema_name
48FROM information_schema.schemata
49WHERE schema_name NOT LIKE 'pg\_%'
50AND schema_name != 'information_schema'
51SQL,
52 );
53 }
54
55 public function createSchemaConfig(): SchemaConfig
56 {
57 $config = parent::createSchemaConfig();
58
59 $config->setName($this->getCurrentSchema());
60
61 return $config;
62 }
63
64 /**
65 * Returns the name of the current schema.
66 *
67 * @throws Exception
68 */
69 protected function getCurrentSchema(): ?string
70 {
71 return $this->currentSchema ??= $this->determineCurrentSchema();
72 }
73
74 /**
75 * Determines the name of the current schema.
76 *
77 * @throws Exception
78 */
79 protected function determineCurrentSchema(): string
80 {
81 $currentSchema = $this->connection->fetchOne('SELECT current_schema()');
82 assert(is_string($currentSchema));
83
84 return $currentSchema;
85 }
86
87 /**
88 * {@inheritDoc}
89 */
90 protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
91 {
92 $onUpdate = null;
93 $onDelete = null;
94
95 if (
96 preg_match(
97 '(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))',
98 $tableForeignKey['condef'],
99 $match,
100 ) === 1
101 ) {
102 $onUpdate = $match[1];
103 }
104
105 if (
106 preg_match(
107 '(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))',
108 $tableForeignKey['condef'],
109 $match,
110 ) === 1
111 ) {
112 $onDelete = $match[1];
113 }
114
115 $result = preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values);
116 assert($result === 1);
117
118 // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
119 // the idea to trim them here.
120 $localColumns = array_map('trim', explode(',', $values[1]));
121 $foreignColumns = array_map('trim', explode(',', $values[3]));
122 $foreignTable = $values[2];
123
124 return new ForeignKeyConstraint(
125 $localColumns,
126 $foreignTable,
127 $foreignColumns,
128 $tableForeignKey['conname'],
129 ['onUpdate' => $onUpdate, 'onDelete' => $onDelete],
130 );
131 }
132
133 /**
134 * {@inheritDoc}
135 */
136 protected function _getPortableViewDefinition(array $view): View
137 {
138 return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']);
139 }
140
141 /**
142 * {@inheritDoc}
143 */
144 protected function _getPortableTableDefinition(array $table): string
145 {
146 $currentSchema = $this->getCurrentSchema();
147
148 if ($table['schema_name'] === $currentSchema) {
149 return $table['table_name'];
150 }
151
152 return $table['schema_name'] . '.' . $table['table_name'];
153 }
154
155 /**
156 * {@inheritDoc}
157 *
158 * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
159 */
160 protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array
161 {
162 $buffer = [];
163 foreach ($tableIndexes as $row) {
164 $colNumbers = array_map('intval', explode(' ', $row['indkey']));
165 $columnNameSql = sprintf(
166 'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
167 $row['indrelid'],
168 implode(' ,', $colNumbers),
169 );
170
171 $indexColumns = $this->connection->fetchAllAssociative($columnNameSql);
172
173 // required for getting the order of the columns right.
174 foreach ($colNumbers as $colNum) {
175 foreach ($indexColumns as $colRow) {
176 if ($colNum !== $colRow['attnum']) {
177 continue;
178 }
179
180 $buffer[] = [
181 'key_name' => $row['relname'],
182 'column_name' => trim($colRow['attname']),
183 'non_unique' => ! $row['indisunique'],
184 'primary' => $row['indisprimary'],
185 'where' => $row['where'],
186 ];
187 }
188 }
189 }
190
191 return parent::_getPortableTableIndexesList($buffer, $tableName);
192 }
193
194 /**
195 * {@inheritDoc}
196 */
197 protected function _getPortableDatabaseDefinition(array $database): string
198 {
199 return $database['datname'];
200 }
201
202 /**
203 * {@inheritDoc}
204 */
205 protected function _getPortableSequenceDefinition(array $sequence): Sequence
206 {
207 if ($sequence['schemaname'] !== 'public') {
208 $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
209 } else {
210 $sequenceName = $sequence['relname'];
211 }
212
213 return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
214 }
215
216 /**
217 * {@inheritDoc}
218 */
219 protected function _getPortableTableColumnDefinition(array $tableColumn): Column
220 {
221 $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
222
223 $length = null;
224
225 if (
226 in_array(strtolower($tableColumn['type']), ['varchar', 'bpchar'], true)
227 && preg_match('/\((\d*)\)/', $tableColumn['complete_type'], $matches) === 1
228 ) {
229 $length = (int) $matches[1];
230 }
231
232 $autoincrement = $tableColumn['attidentity'] === 'd';
233
234 $matches = [];
235
236 assert(array_key_exists('default', $tableColumn));
237 assert(array_key_exists('complete_type', $tableColumn));
238
239 if ($tableColumn['default'] !== null) {
240 if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches) === 1) {
241 $tableColumn['default'] = $matches[1];
242 } elseif (preg_match('/^NULL::/', $tableColumn['default']) === 1) {
243 $tableColumn['default'] = null;
244 }
245 }
246
247 if ($length === -1 && isset($tableColumn['atttypmod'])) {
248 $length = $tableColumn['atttypmod'] - 4;
249 }
250
251 if ((int) $length <= 0) {
252 $length = null;
253 }
254
255 $fixed = false;
256
257 if (! isset($tableColumn['name'])) {
258 $tableColumn['name'] = '';
259 }
260
261 $precision = null;
262 $scale = 0;
263 $jsonb = null;
264
265 $dbType = strtolower($tableColumn['type']);
266 if (
267 $tableColumn['domain_type'] !== null
268 && $tableColumn['domain_type'] !== ''
269 && ! $this->platform->hasDoctrineTypeMappingFor($tableColumn['type'])
270 ) {
271 $dbType = strtolower($tableColumn['domain_type']);
272 $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
273 }
274
275 $type = $this->platform->getDoctrineTypeMapping($dbType);
276
277 switch ($dbType) {
278 case 'smallint':
279 case 'int2':
280 case 'int':
281 case 'int4':
282 case 'integer':
283 case 'bigint':
284 case 'int8':
285 $length = null;
286 break;
287
288 case 'bool':
289 case 'boolean':
290 if ($tableColumn['default'] === 'true') {
291 $tableColumn['default'] = true;
292 }
293
294 if ($tableColumn['default'] === 'false') {
295 $tableColumn['default'] = false;
296 }
297
298 $length = null;
299 break;
300
301 case 'json':
302 case 'text':
303 case '_varchar':
304 case 'varchar':
305 $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']);
306 break;
307
308 case 'char':
309 case 'bpchar':
310 $fixed = true;
311 break;
312
313 case 'float':
314 case 'float4':
315 case 'float8':
316 case 'double':
317 case 'double precision':
318 case 'real':
319 case 'decimal':
320 case 'money':
321 case 'numeric':
322 if (
323 preg_match(
324 '([A-Za-z]+\(([0-9]+),([0-9]+)\))',
325 $tableColumn['complete_type'],
326 $match,
327 ) === 1
328 ) {
329 $precision = (int) $match[1];
330 $scale = (int) $match[2];
331 $length = null;
332 }
333
334 break;
335
336 case 'year':
337 $length = null;
338 break;
339
340 // PostgreSQL 9.4+ only
341 case 'jsonb':
342 $jsonb = true;
343 break;
344 }
345
346 if (
347 is_string($tableColumn['default']) && preg_match(
348 "('([^']+)'::)",
349 $tableColumn['default'],
350 $match,
351 ) === 1
352 ) {
353 $tableColumn['default'] = $match[1];
354 }
355
356 $options = [
357 'length' => $length,
358 'notnull' => (bool) $tableColumn['isnotnull'],
359 'default' => $tableColumn['default'],
360 'precision' => $precision,
361 'scale' => $scale,
362 'fixed' => $fixed,
363 'autoincrement' => $autoincrement,
364 ];
365
366 if (isset($tableColumn['comment'])) {
367 $options['comment'] = $tableColumn['comment'];
368 }
369
370 $column = new Column($tableColumn['field'], Type::getType($type), $options);
371
372 if (! empty($tableColumn['collation'])) {
373 $column->setPlatformOption('collation', $tableColumn['collation']);
374 }
375
376 if ($column->getType() instanceof JsonType) {
377 $column->setPlatformOption('jsonb', $jsonb);
378 }
379
380 return $column;
381 }
382
383 /**
384 * Parses a default value expression as given by PostgreSQL
385 */
386 private function parseDefaultExpression(?string $default): ?string
387 {
388 if ($default === null) {
389 return $default;
390 }
391
392 return str_replace("''", "'", $default);
393 }
394
395 protected function selectTableNames(string $databaseName): Result
396 {
397 $sql = <<<'SQL'
398SELECT quote_ident(table_name) AS table_name,
399 table_schema AS schema_name
400FROM information_schema.tables
401WHERE table_catalog = ?
402 AND table_schema NOT LIKE 'pg\_%'
403 AND table_schema != 'information_schema'
404 AND table_name != 'geometry_columns'
405 AND table_name != 'spatial_ref_sys'
406 AND table_type = 'BASE TABLE'
407SQL;
408
409 return $this->connection->executeQuery($sql, [$databaseName]);
410 }
411
412 protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
413 {
414 $sql = 'SELECT';
415
416 if ($tableName === null) {
417 $sql .= ' c.relname AS table_name, n.nspname AS schema_name,';
418 }
419
420 $sql .= <<<'SQL'
421 a.attnum,
422 quote_ident(a.attname) AS field,
423 t.typname AS type,
424 format_type(a.atttypid, a.atttypmod) AS complete_type,
425 (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation,
426 (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
427 (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
428 pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
429 a.attnotnull AS isnotnull,
430 a.attidentity,
431 (SELECT 't'
432 FROM pg_index
433 WHERE c.oid = pg_index.indrelid
434 AND pg_index.indkey[0] = a.attnum
435 AND pg_index.indisprimary = 't'
436 ) AS pri,
437 (SELECT pg_get_expr(adbin, adrelid)
438 FROM pg_attrdef
439 WHERE c.oid = pg_attrdef.adrelid
440 AND pg_attrdef.adnum=a.attnum
441 ) AS default,
442 (SELECT pg_description.description
443 FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
444 ) AS comment
445 FROM pg_attribute a
446 INNER JOIN pg_class c
447 ON c.oid = a.attrelid
448 INNER JOIN pg_type t
449 ON t.oid = a.atttypid
450 INNER JOIN pg_namespace n
451 ON n.oid = c.relnamespace
452 LEFT JOIN pg_depend d
453 ON d.objid = c.oid
454 AND d.deptype = 'e'
455 AND d.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_class')
456SQL;
457
458 $conditions = array_merge([
459 'a.attnum > 0',
460 "c.relkind = 'r'",
461 'd.refobjid IS NULL',
462 ], $this->buildQueryConditions($tableName));
463
464 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY a.attnum';
465
466 return $this->connection->executeQuery($sql);
467 }
468
469 protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
470 {
471 $sql = 'SELECT';
472
473 if ($tableName === null) {
474 $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,';
475 }
476
477 $sql .= <<<'SQL'
478 quote_ident(ic.relname) AS relname,
479 i.indisunique,
480 i.indisprimary,
481 i.indkey,
482 i.indrelid,
483 pg_get_expr(indpred, indrelid) AS "where"
484 FROM pg_index i
485 JOIN pg_class AS tc ON tc.oid = i.indrelid
486 JOIN pg_namespace tn ON tn.oid = tc.relnamespace
487 JOIN pg_class AS ic ON ic.oid = i.indexrelid
488 WHERE ic.oid IN (
489 SELECT indexrelid
490 FROM pg_index i, pg_class c, pg_namespace n
491SQL;
492
493 $conditions = array_merge([
494 'c.oid = i.indrelid',
495 'c.relnamespace = n.oid',
496 ], $this->buildQueryConditions($tableName));
497
498 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ')';
499
500 return $this->connection->executeQuery($sql);
501 }
502
503 protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
504 {
505 $sql = 'SELECT';
506
507 if ($tableName === null) {
508 $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,';
509 }
510
511 $sql .= <<<'SQL'
512 quote_ident(r.conname) as conname,
513 pg_get_constraintdef(r.oid, true) as condef
514 FROM pg_constraint r
515 JOIN pg_class AS tc ON tc.oid = r.conrelid
516 JOIN pg_namespace tn ON tn.oid = tc.relnamespace
517 WHERE r.conrelid IN
518 (
519 SELECT c.oid
520 FROM pg_class c, pg_namespace n
521SQL;
522
523 $conditions = array_merge(['n.oid = c.relnamespace'], $this->buildQueryConditions($tableName));
524
525 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ") AND r.contype = 'f'";
526
527 return $this->connection->executeQuery($sql);
528 }
529
530 /**
531 * {@inheritDoc}
532 */
533 protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
534 {
535 $sql = <<<'SQL'
536SELECT c.relname,
537 CASE c.relpersistence WHEN 'u' THEN true ELSE false END as unlogged,
538 obj_description(c.oid, 'pg_class') AS comment
539FROM pg_class c
540 INNER JOIN pg_namespace n
541 ON n.oid = c.relnamespace
542SQL;
543
544 $conditions = array_merge(["c.relkind = 'r'"], $this->buildQueryConditions($tableName));
545
546 $sql .= ' WHERE ' . implode(' AND ', $conditions);
547
548 return $this->connection->fetchAllAssociativeIndexed($sql);
549 }
550
551 /** @return list<string> */
552 private function buildQueryConditions(?string $tableName): array
553 {
554 $conditions = [];
555
556 if ($tableName !== null) {
557 if (str_contains($tableName, '.')) {
558 [$schemaName, $tableName] = explode('.', $tableName);
559 $conditions[] = 'n.nspname = ' . $this->platform->quoteStringLiteral($schemaName);
560 } else {
561 $conditions[] = 'n.nspname = ANY(current_schemas(false))';
562 }
563
564 $identifier = new Identifier($tableName);
565 $conditions[] = 'c.relname = ' . $this->platform->quoteStringLiteral($identifier->getName());
566 }
567
568 $conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')";
569
570 return $conditions;
571 }
572}