From 7907bfb50165b3951403e2d9882b4f8e0ee26b9e Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 11 Dec 2023 23:01:00 +0330 Subject: [PATCH] [10.x] Get foreign keys of a table (#49264) * get foreign keys * fix tests * fix tests * fix tests * fix tests --- .../Query/Processors/MySqlProcessor.php | 23 +++++++++ .../Query/Processors/PostgresProcessor.php | 37 +++++++++++++++ .../Database/Query/Processors/Processor.php | 11 +++++ .../Query/Processors/SQLiteProcessor.php | 23 +++++++++ .../Query/Processors/SqlServerProcessor.php | 23 +++++++++ src/Illuminate/Database/Schema/Builder.php | 15 ++++++ .../Database/Schema/Grammars/MySqlGrammar.php | 26 ++++++++++ .../Schema/Grammars/PostgresGrammar.php | 30 ++++++++++++ .../Schema/Grammars/SQLiteGrammar.php | 17 +++++++ .../Schema/Grammars/SqlServerGrammar.php | 29 ++++++++++++ .../Database/Schema/MySqlBuilder.php | 17 +++++++ .../Database/Schema/PostgresBuilder.php | 17 +++++++ src/Illuminate/Support/Facades/Schema.php | 1 + .../Database/SchemaBuilderTest.php | 47 +++++++++++++++++++ 14 files changed, 316 insertions(+) diff --git a/src/Illuminate/Database/Query/Processors/MySqlProcessor.php b/src/Illuminate/Database/Query/Processors/MySqlProcessor.php index 7e6c66face3a..091fc80b52b0 100644 --- a/src/Illuminate/Database/Query/Processors/MySqlProcessor.php +++ b/src/Illuminate/Database/Query/Processors/MySqlProcessor.php @@ -63,4 +63,27 @@ public function processIndexes($results) ]; }, $results); } + + /** + * Process the results of a foreign keys query. + * + * @param array $results + * @return array + */ + public function processForeignKeys($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name, + 'columns' => explode(',', $result->columns), + 'foreign_schema' => $result->foreign_schema, + 'foreign_table' => $result->foreign_table, + 'foreign_columns' => explode(',', $result->foreign_columns), + 'on_update' => strtolower($result->on_update), + 'on_delete' => strtolower($result->on_delete), + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php index c45ae4ae57ee..ddfbfe722da2 100755 --- a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php +++ b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php @@ -91,4 +91,41 @@ public function processIndexes($results) ]; }, $results); } + + /** + * Process the results of a foreign keys query. + * + * @param array $results + * @return array + */ + public function processForeignKeys($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name, + 'columns' => explode(',', $result->columns), + 'foreign_schema' => $result->foreign_schema, + 'foreign_table' => $result->foreign_table, + 'foreign_columns' => explode(',', $result->foreign_columns), + 'on_update' => match (strtolower($result->on_update)) { + 'a' => 'no action', + 'r' => 'restrict', + 'c' => 'cascade', + 'n' => 'set null', + 'd' => 'set default', + default => null, + }, + 'on_delete' => match (strtolower($result->on_delete)) { + 'a' => 'no action', + 'r' => 'restrict', + 'c' => 'cascade', + 'n' => 'set null', + 'd' => 'set default', + default => null, + }, + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 7abf156ad662..87a15c6d7a1d 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -99,6 +99,17 @@ public function processIndexes($results) return $results; } + /** + * Process the results of a foreign keys query. + * + * @param array $results + * @return array + */ + public function processForeignKeys($results) + { + return $results; + } + /** * Process the results of a column listing query. * diff --git a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php index 6c6da5567dba..8f5fb98206e0 100644 --- a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php +++ b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php @@ -79,4 +79,27 @@ public function processIndexes($results) return $indexes; } + + /** + * Process the results of a foreign keys query. + * + * @param array $results + * @return array + */ + public function processForeignKeys($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => null, + 'columns' => explode(',', $result->columns), + 'foreign_schema' => null, + 'foreign_table' => $result->foreign_table, + 'foreign_columns' => explode(',', $result->foreign_columns), + 'on_update' => strtolower($result->on_update), + 'on_delete' => strtolower($result->on_delete), + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php b/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php index 15fa4d740745..c089593ed86a 100755 --- a/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php +++ b/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php @@ -121,4 +121,27 @@ public function processIndexes($results) ]; }, $results); } + + /** + * Process the results of a foreign keys query. + * + * @param array $results + * @return array + */ + public function processForeignKeys($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name, + 'columns' => explode(',', $result->columns), + 'foreign_schema' => $result->foreign_schema, + 'foreign_table' => $result->foreign_table, + 'foreign_columns' => explode(',', $result->foreign_columns), + 'on_update' => strtolower(str_replace('_', ' ', $result->on_update)), + 'on_delete' => strtolower(str_replace('_', ' ', $result->on_delete)), + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 07698e7fc206..efcad17fced3 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -357,6 +357,21 @@ public function getIndexes($table) ); } + /** + * Get the foreign keys for a given table. + * + * @param string $table + * @return array + */ + public function getForeignKeys($table) + { + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processForeignKeys( + $this->connection->selectFromWriteConnection($this->grammar->compileForeignKeys($table)) + ); + } + /** * Modify a table on the schema. * diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 43875591e07f..11ec0df9079b 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -185,6 +185,32 @@ public function compileIndexes($database, $table) ); } + /** + * Compile the query to determine the foreign keys. + * + * @param string $database + * @param string $table + * @return string + */ + public function compileForeignKeys($database, $table) + { + return sprintf( + 'select kc.constraint_name as `name`, ' + .'group_concat(kc.column_name order by kc.ordinal_position) as `columns`, ' + .'kc.referenced_table_schema as `foreign_schema`, ' + .'kc.referenced_table_name as `foreign_table`, ' + .'group_concat(kc.referenced_column_name order by kc.ordinal_position) as `foreign_columns`, ' + .'rc.update_rule as `on_update`, ' + .'rc.delete_rule as `on_delete` ' + .'from information_schema.key_column_usage kc join information_schema.referential_constraints rc ' + .'on kc.constraint_schema = rc.constraint_schema and kc.constraint_name = rc.constraint_name ' + .'where kc.table_schema = %s and kc.table_name = %s and kc.referenced_table_name is not null ' + .'group by kc.constraint_name, kc.referenced_table_schema, kc.referenced_table_name, rc.update_rule, rc.delete_rule', + $this->quoteString($database), + $this->quoteString($table) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 9e18f2d9a773..094d49605d19 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -189,6 +189,36 @@ public function compileIndexes($schema, $table) ); } + /** + * Compile the query to determine the foreign keys. + * + * @param string $schema + * @param string $table + * @return string + */ + public function compileForeignKeys($schema, $table) + { + return sprintf( + 'select c.conname as name, ' + ."string_agg(la.attname, ',' order by conseq.ord) as columns, " + .'fn.nspname as foreign_schema, fc.relname as foreign_table, ' + ."string_agg(fa.attname, ',' order by conseq.ord) as foreign_columns, " + .'c.confupdtype as on_update, c.confdeltype as on_delete ' + .'from pg_constraint c ' + .'join pg_class tc on c.conrelid = tc.oid ' + .'join pg_namespace tn on tn.oid = tc.relnamespace ' + .'join pg_class fc on c.confrelid = fc.oid ' + .'join pg_namespace fn on fn.oid = fc.relnamespace ' + .'join lateral unnest(c.conkey) with ordinality as conseq(num, ord) on true ' + .'join pg_attribute la on la.attrelid = c.conrelid and la.attnum = conseq.num ' + .'join pg_attribute fa on fa.attrelid = c.confrelid and fa.attnum = c.confkey[conseq.ord] ' + ."where c.contype = 'f' and tc.relname = %s and tn.nspname = %s " + .'group by c.conname, fn.nspname, fc.relname, c.confupdtype, c.confdeltype', + $this->quoteString($table), + $this->quoteString($schema) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index a008fb73b0c6..e9c7d8c80490 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -146,6 +146,23 @@ public function compileIndexes($table) ); } + /** + * Compile the query to determine the foreign keys. + * + * @param string $table + * @return string + */ + public function compileForeignKeys($table) + { + return sprintf( + 'select group_concat("from") as columns, "table" as foreign_table, ' + .'group_concat("to") as foreign_columns, on_update, on_delete ' + .'from (select * from pragma_foreign_key_list(%s) order by id desc, seq) ' + .'group by id, "table", on_update, on_delete', + $this->wrap(str_replace('.', '__', $table)) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index b4927d5f3432..4b9091a1d6ef 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -189,6 +189,35 @@ public function compileIndexes($table) ); } + /** + * Compile the query to determine the foreign keys. + * + * @param string $table + * @return string + */ + public function compileForeignKeys($table) + { + return sprintf( + 'select fk.name as name, ' + ."string_agg(lc.name, ',') within group (order by fkc.constraint_column_id) as columns, " + .'fs.name as foreign_schema, ft.name as foreign_table, ' + ."string_agg(fc.name, ',') within group (order by fkc.constraint_column_id) as foreign_columns, " + .'fk.update_referential_action_desc as on_update, ' + .'fk.delete_referential_action_desc as on_delete ' + .'from sys.foreign_keys as fk ' + .'join sys.foreign_key_columns as fkc on fkc.constraint_object_id = fk.object_id ' + .'join sys.tables as lt on lt.object_id = fk.parent_object_id ' + .'join sys.schemas as ls on lt.schema_id = ls.schema_id ' + .'join sys.columns as lc on fkc.parent_object_id = lc.object_id and fkc.parent_column_id = lc.column_id ' + .'join sys.tables as ft on ft.object_id = fk.referenced_object_id ' + .'join sys.schemas as fs on ft.schema_id = fs.schema_id ' + .'join sys.columns as fc on fkc.referenced_object_id = fc.object_id and fkc.referenced_column_id = fc.column_id ' + .'where lt.name = %s and ls.name = SCHEMA_NAME() ' + .'group by fk.name, fs.name, ft.name, fk.update_referential_action_desc, fk.delete_referential_action_desc', + $this->quoteString($table) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/MySqlBuilder.php b/src/Illuminate/Database/Schema/MySqlBuilder.php index e51305002480..0c537ba980cd 100755 --- a/src/Illuminate/Database/Schema/MySqlBuilder.php +++ b/src/Illuminate/Database/Schema/MySqlBuilder.php @@ -120,6 +120,23 @@ public function getIndexes($table) ); } + /** + * Get the foreign keys for a given table. + * + * @param string $table + * @return array + */ + public function getForeignKeys($table) + { + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processForeignKeys( + $this->connection->selectFromWriteConnection( + $this->grammar->compileForeignKeys($this->connection->getDatabaseName(), $table) + ) + ); + } + /** * Drop all tables from the database. * diff --git a/src/Illuminate/Database/Schema/PostgresBuilder.php b/src/Illuminate/Database/Schema/PostgresBuilder.php index 0efe5dc62161..4990cb445d98 100755 --- a/src/Illuminate/Database/Schema/PostgresBuilder.php +++ b/src/Illuminate/Database/Schema/PostgresBuilder.php @@ -220,6 +220,23 @@ public function getIndexes($table) ); } + /** + * Get the foreign keys for a given table. + * + * @param string $table + * @return array + */ + public function getForeignKeys($table) + { + [, $schema, $table] = $this->parseSchemaAndTable($table); + + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processForeignKeys( + $this->connection->selectFromWriteConnection($this->grammar->compileForeignKeys($schema, $table)) + ); + } + /** * Get the schemas for the connection. * diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 1bf87ba9a151..934d27e6f508 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -22,6 +22,7 @@ * @method static array getColumnListing(string $table) * @method static array getColumns(string $table) * @method static array getIndexes(string $table) + * @method static array getForeignKeys(string $table) * @method static void table(string $table, \Closure $callback) * @method static void create(string $table, \Closure $callback) * @method static void drop(string $table) diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index 2e79303b0574..ea9a6d905031 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -261,4 +261,51 @@ public function testGetFullTextIndexes() $this->assertTrue(collect($indexes)->contains(fn ($index) => $index['columns'] === ['id'] && $index['primary'])); $this->assertTrue(collect($indexes)->contains('name', 'articles_body_title_fulltext')); } + + public function testGetForeignKeys() + { + Schema::create('users', function (Blueprint $table) { + $table->id(); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->foreignId('user_id')->nullable()->constrained()->cascadeOnUpdate()->nullOnDelete(); + }); + + $foreignKeys = Schema::getForeignKeys('posts'); + + $this->assertCount(1, $foreignKeys); + $this->assertTrue(collect($foreignKeys)->contains( + fn ($foreign) => $foreign['columns'] === ['user_id'] + && $foreign['foreign_table'] === 'users' && $foreign['foreign_columns'] === ['id'] + && $foreign['on_update'] === 'cascade' && $foreign['on_delete'] === 'set null' + )); + } + + public function testGetCompoundForeignKeys() + { + Schema::create('parent', function (Blueprint $table) { + $table->id(); + $table->integer('a'); + $table->integer('b'); + + $table->unique(['b', 'a']); + }); + + Schema::create('child', function (Blueprint $table) { + $table->integer('c'); + $table->integer('d'); + + $table->foreign(['d', 'c'], 'test_fk')->references(['b', 'a'])->on('parent'); + }); + + $foreignKeys = Schema::getForeignKeys('child'); + + $this->assertCount(1, $foreignKeys); + $this->assertTrue(collect($foreignKeys)->contains( + fn ($foreign) => $foreign['columns'] === ['d', 'c'] + && $foreign['foreign_table'] === 'parent' + && $foreign['foreign_columns'] === ['b', 'a'] + )); + } }