diff options
Diffstat (limited to 'vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php')
-rw-r--r-- | vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php | 572 |
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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\DBAL\Schema; | ||
6 | |||
7 | use Doctrine\DBAL\Exception; | ||
8 | use Doctrine\DBAL\Platforms\PostgreSQLPlatform; | ||
9 | use Doctrine\DBAL\Result; | ||
10 | use Doctrine\DBAL\Types\JsonType; | ||
11 | use Doctrine\DBAL\Types\Type; | ||
12 | |||
13 | use function array_change_key_case; | ||
14 | use function array_key_exists; | ||
15 | use function array_map; | ||
16 | use function array_merge; | ||
17 | use function assert; | ||
18 | use function explode; | ||
19 | use function implode; | ||
20 | use function in_array; | ||
21 | use function is_string; | ||
22 | use function preg_match; | ||
23 | use function sprintf; | ||
24 | use function str_contains; | ||
25 | use function str_replace; | ||
26 | use function strtolower; | ||
27 | use function trim; | ||
28 | |||
29 | use const CASE_LOWER; | ||
30 | |||
31 | /** | ||
32 | * PostgreSQL Schema Manager. | ||
33 | * | ||
34 | * @extends AbstractSchemaManager<PostgreSQLPlatform> | ||
35 | */ | ||
36 | class 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' | ||
47 | SELECT schema_name | ||
48 | FROM information_schema.schemata | ||
49 | WHERE schema_name NOT LIKE 'pg\_%' | ||
50 | AND schema_name != 'information_schema' | ||
51 | SQL, | ||
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' | ||
398 | SELECT quote_ident(table_name) AS table_name, | ||
399 | table_schema AS schema_name | ||
400 | FROM information_schema.tables | ||
401 | WHERE 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' | ||
407 | SQL; | ||
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') | ||
456 | SQL; | ||
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 | ||
491 | SQL; | ||
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 | ||
521 | SQL; | ||
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' | ||
536 | SELECT c.relname, | ||
537 | CASE c.relpersistence WHEN 'u' THEN true ELSE false END as unlogged, | ||
538 | obj_description(c.oid, 'pg_class') AS comment | ||
539 | FROM pg_class c | ||
540 | INNER JOIN pg_namespace n | ||
541 | ON n.oid = c.relnamespace | ||
542 | SQL; | ||
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 | } | ||