From f1068e68007d46c8f9b2b1d089b6478d8a5de1b0 Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Thu, 16 Jan 2025 23:52:56 -0800 Subject: [PATCH] Rework foreign key constraint introspection on PostgreSQL --- src/Schema/PostgreSQLSchemaManager.php | 159 ++++++++++++------ .../Schema/PostgreSQLSchemaManagerTest.php | 6 +- 2 files changed, 105 insertions(+), 60 deletions(-) diff --git a/src/Schema/PostgreSQLSchemaManager.php b/src/Schema/PostgreSQLSchemaManager.php index 9404814bddf..8626ac09863 100644 --- a/src/Schema/PostgreSQLSchemaManager.php +++ b/src/Schema/PostgreSQLSchemaManager.php @@ -35,8 +35,21 @@ */ class PostgreSQLSchemaManager extends AbstractSchemaManager { + private const REFERENTIAL_ACTIONS = [ + 'a' => 'NO ACTION', + 'c' => 'CASCADE', + 'd' => 'SET DEFAULT', + 'n' => 'SET NULL', + 'r' => 'RESTRICT', + ]; + private ?string $currentSchema = null; + /** + * The maximum number of columns that can be included in an index. + */ + private ?int $maxIndexKeys = null; + /** * {@inheritDoc} */ @@ -84,53 +97,37 @@ protected function determineCurrentSchema(): string return $currentSchema; } + /** + * Returns the maximum number of columns that can be included in an index. + * + * @link https://www.postgresql.org/docs/current/runtime-config-preset.html#GUC-MAX-INDEX-KEYS + * + * @throws Exception + */ + private function getMaxIndexKeys(): int + { + return $this->maxIndexKeys ??= (int) $this->connection->fetchOne( + <<<'SQL' + SELECT setting FROM pg_settings WHERE name = 'max_index_keys' + SQL, + ); + } + /** * {@inheritDoc} */ protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint { - $onUpdate = null; - $onDelete = null; - - if ( - preg_match( - '(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', - $tableForeignKey['condef'], - $match, - ) === 1 - ) { - $onUpdate = $match[1]; - } - - if ( - preg_match( - '(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', - $tableForeignKey['condef'], - $match, - ) === 1 - ) { - $onDelete = $match[1]; - } - - $result = preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values); - assert($result === 1); - - // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get - // the idea to trim them here. - $localColumns = array_map('trim', explode(',', $values[1])); - $foreignColumns = array_map('trim', explode(',', $values[3])); - $foreignTable = $values[2]; - return new ForeignKeyConstraint( - $localColumns, - $foreignTable, - $foreignColumns, - $tableForeignKey['conname'], + $tableForeignKey['local'], + $tableForeignKey['foreignTable'], + $tableForeignKey['foreign'], + $tableForeignKey['name'], [ - 'onUpdate' => $onUpdate, - 'onDelete' => $onDelete, - 'deferrable' => $tableForeignKey['condeferrable'], - 'deferred' => $tableForeignKey['condeferred'], + 'onUpdate' => $tableForeignKey['onUpdate'], + 'onDelete' => $tableForeignKey['onDelete'], + 'deferrable' => $tableForeignKey['deferrable'], + 'deferred' => $tableForeignKey['deferred'], ], ); } @@ -149,6 +146,39 @@ protected function _getPortableViewDefinition(array $view): View return new View($name, $view['definition']); } + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array + { + $list = []; + foreach ($tableForeignKeys as $value) { + $value = array_change_key_case($value); + if (! isset($list[$value['conname']])) { + $foreignTable = $value['fk_relname']; + if ($value['fk_nspname'] !== $this->getCurrentSchema()) { + $foreignTable = $value['fk_nspname'] . '.' . $foreignTable; + } + + $list[$value['conname']] = [ + 'name' => $value['conname'], + 'local' => [], + 'foreign' => [], + 'foreignTable' => $foreignTable, + 'onUpdate' => self::REFERENTIAL_ACTIONS[$value['confupdtype']], + 'onDelete' => self::REFERENTIAL_ACTIONS[$value['confdeltype']], + 'deferrable' => $value['condeferrable'], + 'deferred' => $value['condeferred'], + ]; + } + + $list[$value['conname']]['local'][] = $value['pk_attname']; + $list[$value['conname']]['foreign'][] = $value['fk_attname']; + } + + return parent::_getPortableTableForeignKeysList($list); + } + /** * {@inheritDoc} */ @@ -527,31 +557,50 @@ protected function selectIndexColumns(string $databaseName, ?string $tableName = protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; + $sql = 'SELECT r.conname'; if ($tableName === null) { - $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,'; + $sql .= ', pkn.nspname AS schema_name, pkc.relname AS table_name'; } $sql .= <<<'SQL' - quote_ident(r.conname) as conname, - pg_get_constraintdef(r.oid, true) as condef, - r.condeferrable, - r.condeferred - FROM pg_constraint r - JOIN pg_class AS tc ON tc.oid = r.conrelid - JOIN pg_namespace tn ON tn.oid = tc.relnamespace - WHERE r.conrelid IN - ( - SELECT c.oid - FROM pg_class c, pg_namespace n -SQL; + , + pka.attname AS pk_attname, + fkn.nspname AS fk_nspname, + fkc.relname AS fk_relname, + fka.attname AS fk_attname, + r.confupdtype, + r.confdeltype, + r.condeferrable, + r.condeferred + FROM pg_constraint r + JOIN + pg_class fkc ON fkc.oid = r.confrelid + JOIN + pg_namespace fkn ON fkn.oid = fkc.relnamespace + JOIN + pg_attribute fka ON fkc.oid = fka.attrelid + JOIN + pg_class pkc ON pkc.oid = r.conrelid + JOIN + pg_namespace pkn ON pkn.oid = pkc.relnamespace + JOIN + pg_attribute pka ON pkc.oid = pka.attrelid + JOIN + generate_series(1, ?) pos(n) + ON fka.attnum = r.confkey[pos.n] + AND pka.attnum = r.conkey[pos.n] + WHERE r.conrelid IN + ( + SELECT c.oid + FROM pg_class c, pg_namespace n + SQL; $conditions = array_merge(['n.oid = c.relnamespace'], $this->buildQueryConditions($tableName)); $sql .= ' WHERE ' . implode(' AND ', $conditions) . ") AND r.contype = 'f'"; - return $this->connection->executeQuery($sql); + return $this->connection->executeQuery($sql, [$this->getMaxIndexKeys()]); } /** diff --git a/tests/Functional/Schema/PostgreSQLSchemaManagerTest.php b/tests/Functional/Schema/PostgreSQLSchemaManagerTest.php index c4ab362e5ac..919d7fd37f1 100644 --- a/tests/Functional/Schema/PostgreSQLSchemaManagerTest.php +++ b/tests/Functional/Schema/PostgreSQLSchemaManagerTest.php @@ -224,11 +224,7 @@ public function testListForeignKeys(): void self::assertEquals(['foreign_key_test' . $i], array_map('strtolower', $fkeys[$i]->getLocalColumns())); self::assertEquals(['id'], array_map('strtolower', $fkeys[$i]->getForeignColumns())); self::assertEquals('test_create_fk2', strtolower($fkeys[0]->getForeignTableName())); - if ($foreignKeys[$i]->getOption('onDelete') === 'NO ACTION') { - self::assertFalse($fkeys[$i]->hasOption('onDelete')); - } else { - self::assertEquals($foreignKeys[$i]->getOption('onDelete'), $fkeys[$i]->getOption('onDelete')); - } + self::assertEquals($foreignKeys[$i]->getOption('onDelete'), $fkeys[$i]->getOption('onDelete')); } }