diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index a7a8b64acb28..c33bc71fb80d 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -1541,6 +1541,51 @@ public function getForeignKeyData(string $table) return $this->_foreignKeyData($this->protectIdentifiers($table, true, false, false)); } + /** + * Converts array of arrays generated by _foreignKeyData() to array of objects + * + * @return array[ + * {constraint_name} => + * stdClass[ + * 'constraint_name' => string, + * 'table_name' => string, + * 'column_name' => string[], + * 'foreign_table_name' => string, + * 'foreign_column_name' => string[], + * 'on_delete' => string, + * 'on_update' => string, + * 'match' => string + * ] + * ] + */ + protected function foreignKeyDataToObjects(array $data) + { + $retVal = []; + + foreach ($data as $row) { + $name = $row['constraint_name']; + + // for sqlite generate name + if ($name === null) { + $name = $row['table_name'] . '_' . implode('_', $row['column_name']) . '_foreign'; + } + + $obj = new stdClass(); + $obj->constraint_name = $name; + $obj->table_name = $row['table_name']; + $obj->column_name = $row['column_name']; + $obj->foreign_table_name = $row['foreign_table_name']; + $obj->foreign_column_name = $row['foreign_column_name']; + $obj->on_delete = $row['on_delete']; + $obj->on_update = $row['on_update']; + $obj->match = $row['match']; + + $retVal[$name] = $obj; + } + + return $retVal; + } + /** * Disables foreign key checks temporarily. */ diff --git a/system/Database/Forge.php b/system/Database/Forge.php index c2738079b78b..eabed06ed6ec 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -176,6 +176,13 @@ class Forge */ protected $dropIndexStr = 'DROP INDEX %s ON %s'; + /** + * Foreign Key Allowed Actions + * + * @var array + */ + protected $fkAllowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT']; + /** * Constructor. */ @@ -401,7 +408,7 @@ public function addField($field) * * @throws DatabaseException */ - public function addForeignKey($fieldName = '', string $tableName = '', $tableField = '', string $onUpdate = '', string $onDelete = '') + public function addForeignKey($fieldName = '', string $tableName = '', $tableField = '', string $onUpdate = '', string $onDelete = '', string $fkName = '') { $fieldName = (array) $fieldName; $tableField = (array) $tableField; @@ -425,6 +432,7 @@ public function addForeignKey($fieldName = '', string $tableName = '', $tableFie 'referenceField' => $tableField, 'onDelete' => strtoupper($onDelete), 'onUpdate' => strtoupper($onUpdate), + 'fkName' => $fkName, ]; return $this; @@ -480,7 +488,7 @@ public function dropForeignKey(string $table, string $foreignName) $sql = sprintf( (string) $this->dropConstraintStr, $this->db->escapeIdentifiers($this->db->DBPrefix . $table), - $this->db->escapeIdentifiers($this->db->DBPrefix . $foreignName) + $this->db->escapeIdentifiers($foreignName) ); if ($sql === '') { @@ -1055,20 +1063,18 @@ protected function _processIndexes(string $table) return $sqls; } + /** + * Generates SQL to process foreign keys + */ protected function _processForeignKeys(string $table): string { $sql = ''; - $allowActions = [ - 'CASCADE', - 'SET NULL', - 'NO ACTION', - 'RESTRICT', - 'SET DEFAULT', - ]; - foreach ($this->foreignKeys as $fkey) { - $nameIndex = $table . '_' . implode('_', $fkey['field']) . '_foreign'; + $nameIndex = $fkey['fkName'] !== '' ? + $fkey['fkName'] : + $table . '_' . implode('_', $fkey['field']) . ($this->db->DBDriver === 'OCI8' ? '_fk' : '_foreign'); + $nameIndexFilled = $this->db->escapeIdentifiers($nameIndex); $foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field'])); $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']); @@ -1077,11 +1083,11 @@ protected function _processForeignKeys(string $table): string $formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)"; $sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled); - if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { + if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $this->fkAllowActions, true)) { $sql .= ' ON DELETE ' . $fkey['onDelete']; } - if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions, true)) { + if ($this->db->DBDriver !== 'OCI8' && $fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $this->fkAllowActions, true)) { $sql .= ' ON UPDATE ' . $fkey['onUpdate']; } } diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 9d2681394581..0e0b89193062 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -482,43 +482,46 @@ protected function _indexData(string $table): array protected function _foreignKeyData(string $table): array { $sql = ' - SELECT - tc.CONSTRAINT_NAME, - tc.TABLE_NAME, - kcu.COLUMN_NAME, - rc.REFERENCED_TABLE_NAME, - kcu.REFERENCED_COLUMN_NAME - FROM information_schema.TABLE_CONSTRAINTS AS tc - INNER JOIN information_schema.REFERENTIAL_CONSTRAINTS AS rc - ON tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME - AND tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA - INNER JOIN information_schema.KEY_COLUMN_USAGE AS kcu - ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME - AND tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA - WHERE - tc.CONSTRAINT_TYPE = ' . $this->escape('FOREIGN KEY') . ' AND - tc.TABLE_SCHEMA = ' . $this->escape($this->database) . ' AND - tc.TABLE_NAME = ' . $this->escape($table); + SELECT + tc.CONSTRAINT_NAME, + tc.TABLE_NAME, + kcu.COLUMN_NAME, + rc.REFERENCED_TABLE_NAME, + kcu.REFERENCED_COLUMN_NAME, + rc.DELETE_RULE, + rc.UPDATE_RULE, + rc.MATCH_OPTION + FROM information_schema.table_constraints AS tc + INNER JOIN information_schema.referential_constraints AS rc + ON tc.constraint_name = rc.constraint_name + AND tc.constraint_schema = rc.constraint_schema + INNER JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.constraint_schema = kcu.constraint_schema + WHERE + tc.constraint_type = ' . $this->escape('FOREIGN KEY') . ' AND + tc.table_schema = ' . $this->escape($this->database) . ' AND + tc.table_name = ' . $this->escape($table); if (($query = $this->query($sql)) === false) { throw new DatabaseException(lang('Database.failGetForeignKeyData')); } - $query = $query->getResultObject(); - $retVal = []; + $query = $query->getResultObject(); + $indexes = []; foreach ($query as $row) { - $obj = new stdClass(); - $obj->constraint_name = $row->CONSTRAINT_NAME; - $obj->table_name = $row->TABLE_NAME; - $obj->column_name = $row->COLUMN_NAME; - $obj->foreign_table_name = $row->REFERENCED_TABLE_NAME; - $obj->foreign_column_name = $row->REFERENCED_COLUMN_NAME; - - $retVal[] = $obj; + $indexes[$row->CONSTRAINT_NAME]['constraint_name'] = $row->CONSTRAINT_NAME; + $indexes[$row->CONSTRAINT_NAME]['table_name'] = $row->TABLE_NAME; + $indexes[$row->CONSTRAINT_NAME]['column_name'][] = $row->COLUMN_NAME; + $indexes[$row->CONSTRAINT_NAME]['foreign_table_name'] = $row->REFERENCED_TABLE_NAME; + $indexes[$row->CONSTRAINT_NAME]['foreign_column_name'][] = $row->REFERENCED_COLUMN_NAME; + $indexes[$row->CONSTRAINT_NAME]['on_delete'] = $row->DELETE_RULE; + $indexes[$row->CONSTRAINT_NAME]['on_update'] = $row->UPDATE_RULE; + $indexes[$row->CONSTRAINT_NAME]['match'] = $row->MATCH_OPTION; } - return $retVal; + return $this->foreignKeyDataToObjects($indexes); } /** diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index 854a36ab3929..73c53b2e107c 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -384,43 +384,44 @@ protected function _indexData(string $table): array protected function _foreignKeyData(string $table): array { $sql = 'SELECT - acc.constraint_name, - acc.table_name, - acc.column_name, - ccu.table_name foreign_table_name, - accu.column_name foreign_column_name - FROM all_cons_columns acc - JOIN all_constraints ac - ON acc.owner = ac.owner - AND acc.constraint_name = ac.constraint_name - JOIN all_constraints ccu - ON ac.r_owner = ccu.owner - AND ac.r_constraint_name = ccu.constraint_name - JOIN all_cons_columns accu - ON accu.constraint_name = ccu.constraint_name - AND accu.table_name = ccu.table_name - WHERE ac.constraint_type = ' . $this->escape('R') . ' - AND acc.table_name = ' . $this->escape($table); + acc.constraint_name, + acc.table_name, + acc.column_name, + ccu.table_name foreign_table_name, + accu.column_name foreign_column_name, + ac.delete_rule + FROM all_cons_columns acc + JOIN all_constraints ac ON acc.owner = ac.owner + AND acc.constraint_name = ac.constraint_name + JOIN all_constraints ccu ON ac.r_owner = ccu.owner + AND ac.r_constraint_name = ccu.constraint_name + JOIN all_cons_columns accu ON accu.constraint_name = ccu.constraint_name + AND accu.position = acc.position + AND accu.table_name = ccu.table_name + WHERE ac.constraint_type = ' . $this->escape('R') . ' + AND acc.table_name = ' . $this->escape($table); + $query = $this->query($sql); if ($query === false) { throw new DatabaseException(lang('Database.failGetForeignKeyData')); } - $query = $query->getResultObject(); - $retVal = []; + $query = $query->getResultObject(); + $indexes = []; foreach ($query as $row) { - $obj = new stdClass(); - $obj->constraint_name = $row->CONSTRAINT_NAME; - $obj->table_name = $row->TABLE_NAME; - $obj->column_name = $row->COLUMN_NAME; - $obj->foreign_table_name = $row->FOREIGN_TABLE_NAME; - $obj->foreign_column_name = $row->FOREIGN_COLUMN_NAME; - $retVal[] = $obj; + $indexes[$row->CONSTRAINT_NAME]['constraint_name'] = $row->CONSTRAINT_NAME; + $indexes[$row->CONSTRAINT_NAME]['table_name'] = $row->TABLE_NAME; + $indexes[$row->CONSTRAINT_NAME]['column_name'][] = $row->COLUMN_NAME; + $indexes[$row->CONSTRAINT_NAME]['foreign_table_name'] = $row->FOREIGN_TABLE_NAME; + $indexes[$row->CONSTRAINT_NAME]['foreign_column_name'][] = $row->FOREIGN_COLUMN_NAME; + $indexes[$row->CONSTRAINT_NAME]['on_delete'] = $row->DELETE_RULE; + $indexes[$row->CONSTRAINT_NAME]['on_update'] = null; + $indexes[$row->CONSTRAINT_NAME]['match'] = null; } - return $retVal; + return $this->foreignKeyDataToObjects($indexes); } /** diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php index add01540177a..8acbd246c063 100644 --- a/system/Database/OCI8/Forge.php +++ b/system/Database/OCI8/Forge.php @@ -83,6 +83,13 @@ class Forge extends BaseForge */ protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s'; + /** + * Foreign Key Allowed Actions + * + * @var array + */ + protected $fkAllowActions = ['CASCADE', 'SET NULL', 'NO ACTION']; + /** * ALTER TABLE * @@ -272,32 +279,4 @@ protected function _dropTable(string $table, bool $ifExists, bool $cascade) return $sql; } - - protected function _processForeignKeys(string $table): string - { - $sql = ''; - - $allowActions = [ - 'CASCADE', - 'SET NULL', - 'NO ACTION', - ]; - - foreach ($this->foreignKeys as $fkey) { - $nameIndex = $table . '_' . implode('_', $fkey['field']) . '_fk'; - $nameIndexFilled = $this->db->escapeIdentifiers($nameIndex); - $foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field'])); - $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']); - $referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField'])); - - $formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)"; - $sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled); - - if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { - $sql .= ' ON DELETE ' . $fkey['onDelete']; - } - } - - return $sql; - } } diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 995719a33c0d..cd6ada29322e 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -326,38 +326,42 @@ protected function _indexData(string $table): array */ protected function _foreignKeyData(string $table): array { - $sql = 'SELECT - tc.constraint_name, tc.table_name, kcu.column_name, - ccu.table_name AS foreign_table_name, - ccu.column_name AS foreign_column_name - FROM information_schema.table_constraints AS tc - JOIN information_schema.key_column_usage AS kcu - ON tc.constraint_name = kcu.constraint_name - JOIN information_schema.constraint_column_usage AS ccu - ON ccu.constraint_name = tc.constraint_name - WHERE constraint_type = ' . $this->escape('FOREIGN KEY') . ' AND - tc.table_name = ' . $this->escape($table); + $sql = 'SELECT c.constraint_name, + x.table_name, + x.column_name, + y.table_name as foreign_table_name, + y.column_name as foreign_column_name, + c.delete_rule, + c.update_rule, + c.match_option + FROM information_schema.referential_constraints c + JOIN information_schema.key_column_usage x + on x.constraint_name = c.constraint_name + JOIN information_schema.key_column_usage y + on y.ordinal_position = x.position_in_unique_constraint + and y.constraint_name = c.unique_constraint_name + WHERE x.table_name = ' . $this->escape($table) . + 'order by c.constraint_name, x.ordinal_position'; if (($query = $this->query($sql)) === false) { throw new DatabaseException(lang('Database.failGetForeignKeyData')); } - $query = $query->getResultObject(); - $retVal = []; + $query = $query->getResultObject(); + $indexes = []; foreach ($query as $row) { - $obj = new stdClass(); - - $obj->constraint_name = $row->constraint_name; - $obj->table_name = $row->table_name; - $obj->column_name = $row->column_name; - $obj->foreign_table_name = $row->foreign_table_name; - $obj->foreign_column_name = $row->foreign_column_name; - - $retVal[] = $obj; + $indexes[$row->constraint_name]['constraint_name'] = $row->constraint_name; + $indexes[$row->constraint_name]['table_name'] = $table; + $indexes[$row->constraint_name]['column_name'][] = $row->column_name; + $indexes[$row->constraint_name]['foreign_table_name'] = $row->foreign_table_name; + $indexes[$row->constraint_name]['foreign_column_name'][] = $row->foreign_column_name; + $indexes[$row->constraint_name]['on_delete'] = $row->delete_rule; + $indexes[$row->constraint_name]['on_update'] = $row->update_rule; + $indexes[$row->constraint_name]['match'] = $row->match_option; } - return $retVal; + return $this->foreignKeyDataToObjects($indexes); } /** diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 1520543261c8..134421610e1e 100755 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -273,43 +273,41 @@ protected function _indexData(string $table): array */ protected function _foreignKeyData(string $table): array { - $sql = 'SELECT ' - . 'f.name as constraint_name, ' - . 'OBJECT_NAME (f.parent_object_id) as table_name, ' - . 'COL_NAME(fc.parent_object_id,fc.parent_column_id) column_name, ' - . 'OBJECT_NAME(f.referenced_object_id) foreign_table_name, ' - . 'COL_NAME(fc.referenced_object_id,fc.referenced_column_id) foreign_column_name ' - . 'FROM ' - . 'sys.foreign_keys AS f ' - . 'INNER JOIN ' - . 'sys.foreign_key_columns AS fc ' - . 'ON f.OBJECT_ID = fc.constraint_object_id ' - . 'INNER JOIN ' - . 'sys.tables t ' - . 'ON t.OBJECT_ID = fc.referenced_object_id ' - . 'WHERE ' - . 'OBJECT_NAME (f.parent_object_id) = ' . $this->escape($table); + $sql = 'SELECT + f.name as constraint_name, + OBJECT_NAME (f.parent_object_id) as table_name, + COL_NAME(fc.parent_object_id,fc.parent_column_id) column_name, + OBJECT_NAME(f.referenced_object_id) foreign_table_name, + COL_NAME(fc.referenced_object_id,fc.referenced_column_id) foreign_column_name, + rc.delete_rule, + rc.update_rule, + rc.match_option + FROM + sys.foreign_keys AS f + INNER JOIN sys.foreign_key_columns AS fc ON f.OBJECT_ID = fc.constraint_object_id + INNER JOIN sys.tables t ON t.OBJECT_ID = fc.referenced_object_id + INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc ON rc.CONSTRAINT_NAME = f.name + WHERE OBJECT_NAME (f.parent_object_id) = ' . $this->escape($table); if (($query = $this->query($sql)) === false) { throw new DatabaseException(lang('Database.failGetForeignKeyData')); } - $query = $query->getResultObject(); - $retVal = []; + $query = $query->getResultObject(); + $indexes = []; foreach ($query as $row) { - $obj = new stdClass(); - - $obj->constraint_name = $row->constraint_name; - $obj->table_name = $row->table_name; - $obj->column_name = $row->column_name; - $obj->foreign_table_name = $row->foreign_table_name; - $obj->foreign_column_name = $row->foreign_column_name; - - $retVal[] = $obj; + $indexes[$row->constraint_name]['constraint_name'] = $row->constraint_name; + $indexes[$row->constraint_name]['table_name'] = $row->table_name; + $indexes[$row->constraint_name]['column_name'][] = $row->column_name; + $indexes[$row->constraint_name]['foreign_table_name'] = $row->foreign_table_name; + $indexes[$row->constraint_name]['foreign_column_name'][] = $row->foreign_column_name; + $indexes[$row->constraint_name]['on_delete'] = $row->delete_rule; + $indexes[$row->constraint_name]['on_update'] = $row->update_rule; + $indexes[$row->constraint_name]['match'] = $row->match_option; } - return $retVal; + return $this->foreignKeyDataToObjects($indexes); } /** diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index 7f8a0d8199e2..e9625018cb00 100755 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -83,6 +83,13 @@ class Forge extends BaseForge 'REAL' => 'FLOAT', ]; + /** + * Foreign Key Allowed Actions + * + * @var array + */ + protected $fkAllowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT']; + /** * CREATE TABLE IF statement * @@ -288,39 +295,6 @@ protected function _processColumn(array $field): string . $field['unique']; } - /** - * Process foreign keys - * - * @param string $table Table name - */ - protected function _processForeignKeys(string $table): string - { - $sql = ''; - - $allowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT']; - - foreach ($this->foreignKeys as $fkey) { - $nameIndex = $table . '_' . implode('_', $fkey['field']) . '_foreign'; - $nameIndexFilled = $this->db->escapeIdentifiers($nameIndex); - $foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field'])); - $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']); - $referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField'])); - - $formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)"; - $sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled); - - if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { - $sql .= ' ON DELETE ' . $fkey['onDelete']; - } - - if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions, true)) { - $sql .= ' ON UPDATE ' . $fkey['onUpdate']; - } - } - - return $sql; - } - /** * Process primary keys */ diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index cdbd5c87218f..37e079411efe 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -333,29 +333,21 @@ protected function _foreignKeyData(string $table): array return []; } - $tables = $this->listTables(); + $query = $this->query("PRAGMA foreign_key_list({$table})")->getResult(); + $indexes = []; - if (empty($tables)) { - return []; - } - - $retVal = []; - - foreach ($tables as $table) { - $query = $this->query("PRAGMA foreign_key_list({$table})")->getResult(); - - foreach ($query as $row) { - $obj = new stdClass(); - $obj->constraint_name = $row->from . ' to ' . $row->table . '.' . $row->to; - $obj->table_name = $table; - $obj->foreign_table_name = $row->table; - $obj->sequence = $row->seq; - - $retVal[] = $obj; - } + foreach ($query as $row) { + $indexes[$row->id]['constraint_name'] = null; + $indexes[$row->id]['table_name'] = $table; + $indexes[$row->id]['foreign_table_name'] = $row->table; + $indexes[$row->id]['column_name'][] = $row->from; + $indexes[$row->id]['foreign_column_name'][] = $row->to; + $indexes[$row->id]['on_delete'] = $row->on_delete; + $indexes[$row->id]['on_update'] = $row->on_update; + $indexes[$row->id]['match'] = $row->match; } - return $retVal; + return $this->foreignKeyDataToObjects($indexes); } /** diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index ef28a9e659e7..5538777f610b 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -262,4 +262,13 @@ public function dropPrimaryKey(string $table): bool ->dropPrimaryKey() ->run(); } + + public function addForeignKey($fieldName = '', string $tableName = '', $tableField = '', string $onUpdate = '', string $onDelete = '', string $fkName = '') + { + if ($fkName === '') { + return parent::addForeignKey($fieldName, $tableName, $tableField, $onUpdate, $onDelete, $fkName); + } + + throw new DatabaseException('SQLite does not support foreign key names. CodeIgniter will refer to them in the format: prefix_table_column_referencecolumn_foreign'); + } } diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php index 0f6911b62c30..2c142086a1d5 100644 --- a/system/Database/SQLite3/Table.php +++ b/system/Database/SQLite3/Table.php @@ -214,23 +214,14 @@ public function dropPrimaryKey(): Table * * @return Table */ - public function dropForeignKey(string $column) + public function dropForeignKey(string $foreignName) { if (empty($this->foreignKeys)) { return $this; } - for ($i = 0; $i < count($this->foreignKeys); $i++) { - if ($this->foreignKeys[$i]->table_name !== $this->tableName) { - continue; - } - - // The column name should be the first thing in the constraint name - if (strpos($this->foreignKeys[$i]->constraint_name, $column) !== 0) { - continue; - } - - unset($this->foreignKeys[$i]); + if (isset($this->foreignKeys[$foreignName])) { + unset($this->foreignKeys[$foreignName]); } return $this; diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 0dd6ae86e89a..978550939b09 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -34,8 +34,15 @@ final class ForgeTest extends CIUnitTestCase protected function setUp(): void { - parent::setUp(); $this->forge = Database::forge($this->DBGroup); + + // when running locally if one of these tables isn't dropped it may cause error + $this->forge->dropTable('forge_test_invoices', true); + $this->forge->dropTable('forge_test_inv', true); + $this->forge->dropTable('forge_test_users', true); + $this->forge->dropTable('actions', true); + + parent::setUp(); } public function testCreateDatabase() @@ -433,6 +440,7 @@ public function testDropTableWithEmptyName() public function testForeignKey() { + $this->forge->dropTable('forge_test_invoices', true); $this->forge->dropTable('forge_test_users', true); $attributes = []; @@ -472,28 +480,21 @@ public function testForeignKey() $this->forge->addForeignKey('users_id', 'forge_test_users', 'id', 'CASCADE', 'CASCADE'); $tableName = 'forge_test_invoices'; - if ($this->db->DBDriver === 'OCI8') { - $tableName = 'forge_test_inv'; - } $this->forge->createTable($tableName, true, $attributes); $foreignKeyData = $this->db->getForeignKeyData($tableName); - if ($this->db->DBDriver === 'SQLite3') { - $this->assertSame($foreignKeyData[0]->constraint_name, 'users_id to db_forge_test_users.id'); - $this->assertSame($foreignKeyData[0]->sequence, 0); - } elseif ($this->db->DBDriver === 'OCI8') { - $this->assertSame($foreignKeyData[0]->constraint_name, $this->db->DBPrefix . 'forge_test_inv_users_id_fk'); - $this->assertSame($foreignKeyData[0]->column_name, 'users_id'); - $this->assertSame($foreignKeyData[0]->foreign_column_name, 'id'); - } else { - $this->assertSame($foreignKeyData[0]->constraint_name, $this->db->DBPrefix . 'forge_test_invoices_users_id_foreign'); - $this->assertSame($foreignKeyData[0]->column_name, 'users_id'); - $this->assertSame($foreignKeyData[0]->foreign_column_name, 'id'); + $foreignKeyName = $this->db->DBPrefix . $tableName . '_users_id_foreign'; + if ($this->db->DBDriver === 'OCI8') { + $foreignKeyName = $this->db->DBPrefix . $tableName . '_users_id_fk'; } - $this->assertSame($foreignKeyData[0]->table_name, $this->db->DBPrefix . $tableName); - $this->assertSame($foreignKeyData[0]->foreign_table_name, $this->db->DBPrefix . 'forge_test_users'); + + $this->assertSame($foreignKeyData[$foreignKeyName]->constraint_name, $foreignKeyName); + $this->assertSame($foreignKeyData[$foreignKeyName]->column_name, ['users_id']); + $this->assertSame($foreignKeyData[$foreignKeyName]->foreign_column_name, ['id']); + $this->assertSame($foreignKeyData[$foreignKeyName]->table_name, $this->db->DBPrefix . $tableName); + $this->assertSame($foreignKeyData[$foreignKeyName]->foreign_table_name, $this->db->DBPrefix . 'forge_test_users'); $this->forge->dropTable($tableName, true); $this->forge->dropTable('forge_test_users', true); @@ -515,22 +516,24 @@ public function testForeignKeyAddingWithStringFields() '`name` VARCHAR(255) NOT NULL', ])->createTable('forge_test_users', true, $attributes); + $foreignKeyName = 'my_custom_fk'; + $this->forge ->addField([ '`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY', '`users_id` INT(11) NOT NULL', '`name` VARCHAR(255) NOT NULL', ]) - ->addForeignKey('users_id', 'forge_test_users', 'id', 'CASCADE', 'CASCADE') + ->addForeignKey('users_id', 'forge_test_users', 'id', 'CASCADE', 'CASCADE', $foreignKeyName) ->createTable('forge_test_invoices', true, $attributes); - $foreignKeyData = $this->db->getForeignKeyData('forge_test_invoices')[0]; + $foreignKeyData = $this->db->getForeignKeyData('forge_test_invoices'); - $this->assertSame($this->db->DBPrefix . 'forge_test_invoices_users_id_foreign', $foreignKeyData->constraint_name); - $this->assertSame('users_id', $foreignKeyData->column_name); - $this->assertSame('id', $foreignKeyData->foreign_column_name); - $this->assertSame($this->db->DBPrefix . 'forge_test_invoices', $foreignKeyData->table_name); - $this->assertSame($this->db->DBPrefix . 'forge_test_users', $foreignKeyData->foreign_table_name); + $this->assertSame($foreignKeyName, $foreignKeyData[$foreignKeyName]->constraint_name); + $this->assertSame(['users_id'], $foreignKeyData[$foreignKeyName]->column_name); + $this->assertSame(['id'], $foreignKeyData[$foreignKeyName]->foreign_column_name); + $this->assertSame($this->db->DBPrefix . 'forge_test_invoices', $foreignKeyData[$foreignKeyName]->table_name); + $this->assertSame($this->db->DBPrefix . 'forge_test_users', $foreignKeyData[$foreignKeyName]->foreign_table_name); $this->forge->dropTable('forge_test_invoices', true); $this->forge->dropTable('forge_test_users', true); @@ -541,6 +544,9 @@ public function testForeignKeyAddingWithStringFields() */ public function testCompositeForeignKey() { + $this->forge->dropTable('forge_test_invoices', true); + $this->forge->dropTable('forge_test_users', true); + $attributes = []; if ($this->db->DBDriver === 'MySQLi') { @@ -564,10 +570,7 @@ public function testCompositeForeignKey() $this->forge->addPrimaryKey(['id', 'second_id']); $this->forge->createTable('forge_test_users', true, $attributes); - $forgeTestInvoicesTableName = 'forge_test_invoices'; - $userIdColumnName = 'users_id'; - $userSecondIdColumnName = 'users_second_id'; - $fields = [ + $fields = [ 'id' => [ 'type' => 'INTEGER', 'constraint' => 11, @@ -576,58 +579,39 @@ public function testCompositeForeignKey() 'type' => 'VARCHAR', 'constraint' => 255, ], - ]; - - if ($this->db->DBDriver === 'OCI8') { - $userIdColumnName = 'uid'; - $userSecondIdColumnName = 'usid'; - $forgeTestInvoicesTableName = 'forge_test_inv'; - } - - $fields[$userIdColumnName] = [ - 'type' => 'INTEGER', - 'constraint' => 11, - ]; - - $fields[$userSecondIdColumnName] = [ - 'type' => 'VARCHAR', - 'constraint' => 50, + 'users_id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'users_second_id' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + ], ]; $this->forge->addField($fields); $this->forge->addPrimaryKey('id'); - $this->forge->addForeignKey([$userIdColumnName, $userSecondIdColumnName], 'forge_test_users', ['id', 'second_id'], 'CASCADE', 'CASCADE'); - $this->forge->createTable($forgeTestInvoicesTableName, true, $attributes); - - $foreignKeyData = $this->db->getForeignKeyData($forgeTestInvoicesTableName); + $foreignKeyName = 'my_custom_fk'; if ($this->db->DBDriver === 'SQLite3') { - $this->assertSame('users_id to db_forge_test_users.id', $foreignKeyData[0]->constraint_name); - $this->assertSame(0, $foreignKeyData[0]->sequence); - $this->assertSame('users_second_id to db_forge_test_users.second_id', $foreignKeyData[1]->constraint_name); - $this->assertSame(1, $foreignKeyData[1]->sequence); - } elseif ($this->db->DBDriver === 'OCI8') { - $haystack = [$userIdColumnName, $userSecondIdColumnName]; - $this->assertSame($this->db->DBPrefix . 'forge_test_inv_uid_usid_fk', $foreignKeyData[0]->constraint_name); - $this->assertContains($foreignKeyData[0]->column_name, $haystack); + $foreignKeyName = $this->db->DBPrefix . 'forge_test_invoices_users_id_users_second_id_foreign'; + } - $secondIdKey = 1; - $this->assertSame($this->db->DBPrefix . 'forge_test_inv_uid_usid_fk', $foreignKeyData[$secondIdKey]->constraint_name); - $this->assertContains($foreignKeyData[$secondIdKey]->column_name, $haystack); - } else { - $haystack = [$userIdColumnName, $userSecondIdColumnName]; - $this->assertSame($this->db->DBPrefix . 'forge_test_invoices_users_id_users_second_id_foreign', $foreignKeyData[0]->constraint_name); - $this->assertContains($foreignKeyData[0]->column_name, $haystack); + $this->forge->addForeignKey(['users_id', 'users_second_id'], 'forge_test_users', ['id', 'second_id'], 'CASCADE', 'CASCADE', ($this->db->DBDriver !== 'SQLite3' ? $foreignKeyName : '')); - $secondIdKey = $this->db->DBDriver === 'Postgre' ? 2 : 1; - $this->assertSame($this->db->DBPrefix . 'forge_test_invoices_users_id_users_second_id_foreign', $foreignKeyData[$secondIdKey]->constraint_name); - $this->assertContains($foreignKeyData[$secondIdKey]->column_name, $haystack); - } - $this->assertSame($this->db->DBPrefix . $forgeTestInvoicesTableName, $foreignKeyData[0]->table_name); - $this->assertSame($this->db->DBPrefix . 'forge_test_users', $foreignKeyData[0]->foreign_table_name); + $this->forge->createTable('forge_test_invoices', true, $attributes); - $this->forge->dropTable($forgeTestInvoicesTableName, true); + $foreignKeyData = $this->db->getForeignKeyData('forge_test_invoices'); + + $haystack = ['users_id', 'users_second_id']; + $this->assertSame($foreignKeyName, $foreignKeyData[$foreignKeyName]->constraint_name); + $this->assertSame($foreignKeyData[$foreignKeyName]->column_name, $haystack); + + $this->assertSame($this->db->DBPrefix . 'forge_test_invoices', $foreignKeyData[$foreignKeyName]->table_name); + $this->assertSame($this->db->DBPrefix . 'forge_test_users', $foreignKeyData[$foreignKeyName]->foreign_table_name); + + $this->forge->dropTable('forge_test_invoices', true); $this->forge->dropTable('forge_test_users', true); } @@ -637,7 +621,11 @@ public function testCompositeForeignKey() public function testCompositeForeignKeyFieldNotExistException() { $this->expectException(DatabaseException::class); - $this->expectExceptionMessage('Field "user_id, user_second_id" not found.'); + if ($this->db->DBDriver === 'SQLite3') { + $this->expectExceptionMessage('SQLite does not support foreign key names. CodeIgniter will refer to them in the format: prefix_table_column_referencecolumn_foreign'); + } else { + $this->expectExceptionMessage('Field "user_id, user_second_id" not found.'); + } $attributes = []; @@ -681,7 +669,10 @@ public function testCompositeForeignKeyFieldNotExistException() ], ]); $this->forge->addKey('id', true); - $this->forge->addForeignKey(['user_id', 'user_second_id'], 'forge_test_users', ['id', 'second_id'], 'CASCADE', 'CASCADE'); + + $foreignKeyName = 'forge_test_invoices_fk'; + + $this->forge->addForeignKey(['user_id', 'user_second_id'], 'forge_test_users', ['id', 'second_id'], 'CASCADE', 'CASCADE', $foreignKeyName); $this->forge->createTable('forge_test_invoices', true, $attributes); } @@ -689,7 +680,11 @@ public function testCompositeForeignKeyFieldNotExistException() public function testForeignKeyFieldNotExistException() { $this->expectException(DatabaseException::class); - $this->expectExceptionMessage('Field "user_id" not found.'); + if ($this->db->DBDriver === 'SQLite3') { + $this->expectExceptionMessage('SQLite does not support foreign key names. CodeIgniter will refer to them in the format: prefix_table_column_referencecolumn_foreign'); + } else { + $this->expectExceptionMessage('Field "user_id" not found.'); + } $attributes = []; @@ -725,13 +720,17 @@ public function testForeignKeyFieldNotExistException() ], ]); $this->forge->addKey('id', true); - $this->forge->addForeignKey('user_id', 'forge_test_users', 'id', 'CASCADE', 'CASCADE'); + + $foreignKeyName = 'forge_test_invoices_fk'; + + $this->forge->addForeignKey('user_id', 'forge_test_users', 'id', 'CASCADE', 'CASCADE', $foreignKeyName); $this->forge->createTable('forge_test_invoices', true, $attributes); } public function testDropForeignKey() { + $this->forge->dropTable('forge_test_invoices', true); $this->forge->dropTable('forge_test_users', true); $attributes = []; @@ -768,15 +767,17 @@ public function testDropForeignKey() ], ]); $this->forge->addKey('id', true); - $this->forge->addForeignKey('users_id', 'forge_test_users', 'id', 'CASCADE', 'CASCADE'); - $tableName = 'forge_test_invoices'; - $foreignKeyName = 'forge_test_invoices_users_id_foreign'; - if ($this->db->DBDriver === 'OCI8') { - $tableName = 'forge_test_inv'; - $foreignKeyName = 'forge_test_inv_users_id_fk'; + $foreignKeyName = 'forge_test_invoices_fk'; + + if ($this->db->DBDriver === 'SQLite3') { + $foreignKeyName = $this->db->DBPrefix . 'forge_test_invoices_users_id_foreign'; } + $this->forge->addForeignKey('users_id', 'forge_test_users', 'id', 'CASCADE', 'CASCADE', ($this->db->DBDriver !== 'SQLite3' ? $foreignKeyName : '')); + + $tableName = 'forge_test_invoices'; + $this->forge->createTable($tableName, true, $attributes); $this->forge->dropForeignKey($tableName, $foreignKeyName); @@ -1034,8 +1035,8 @@ public function testAddFields() ], ]; - // Sequence id may change - $this->assertMatchesRegularExpression('/"ORACLE"."ISEQ\\$\\$_\d+".nextval/', $fieldsData[0]->default); + // Sequence id may change - MAY USE "SYSTEM" instead of "ORACLE" + $this->assertMatchesRegularExpression('/"(ORACLE|SYSTEM)"."ISEQ\\$\\$_\d+".nextval/', $fieldsData[0]->default); $expected[0]['default'] = $fieldsData[0]->default; } else { $this->fail(sprintf('DB driver "%s" is not supported.', $this->db->DBDriver)); diff --git a/tests/system/Database/Live/SQLite/AlterTableTest.php b/tests/system/Database/Live/SQLite/AlterTableTest.php index 0d3da18d1a81..61696ec9d573 100644 --- a/tests/system/Database/Live/SQLite/AlterTableTest.php +++ b/tests/system/Database/Live/SQLite/AlterTableTest.php @@ -224,11 +224,11 @@ public function testDropForeignKeySuccess() $this->createTable('aliens'); $keys = $this->db->getForeignKeyData('aliens'); - $this->assertSame('key_id to aliens_fk.id', $keys[0]->constraint_name); + $this->assertSame($this->db->DBPrefix . 'aliens_key_id_foreign', $keys[$this->db->DBPrefix . 'aliens_key_id_foreign']->constraint_name); $result = $this->table ->fromTable('aliens') - ->dropForeignKey('key_id') + ->dropForeignKey('aliens_key_id_foreign') ->run(); $this->assertTrue($result); diff --git a/user_guide_src/source/changelogs/v4.3.0.rst b/user_guide_src/source/changelogs/v4.3.0.rst index 6a794ec5b157..fa0dc83e3ea9 100644 --- a/user_guide_src/source/changelogs/v4.3.0.rst +++ b/user_guide_src/source/changelogs/v4.3.0.rst @@ -45,6 +45,7 @@ Others - ``RouteCollection::resetRoutes()`` resets Auto-Discovery of Routes. Previously once discovered, RouteCollection never discover Routes files again even if ``RouteCollection::resetRoutes()`` is called. - ``CITestStreamFilter::$buffer = ''`` no longer causes the filter to be registered to listen for streams. Now there is a ``CITestStreamFilter::registration()`` method for this. See :ref:`upgrade-430-stream-filter` for details. +- The data structure returned by ``BaseConnection::getForeignKeyData()`` has been changed. .. _v430-interface-changes: @@ -88,6 +89,7 @@ Others - The return type of ``CodeIgniter\Database\Database::loadForge()`` has been changed to ``Forge``. - The return type of ``CodeIgniter\Database\Database::loadUtils()`` has been changed to ``BaseUtils``. +- Parameter ``$column`` has changed in ``Table::dropForeignKey()`` to ``$foreignName``. Enhancements ************ @@ -117,6 +119,8 @@ Database - ``BaseConnection::escape()`` now excludes the ``RawSql`` data type. This allows passing SQL strings into data. - The new method ``Forge::dropPrimaryKey()`` allows dropping the primary key on a table. See :ref:`dropping-a-primary-key`. - Improved the SQL structure for ``Builder::updateBatch()``. See :ref:`update-batch` for the details. +- Improved data returned by ``BaseConnection::getForeignKeyData()``. All DBMS returns the same structure. +- ``Forge::addForeignKey()`` now includes a name parameter to manual set foreign key names. Not supported in SQLite3. Model ===== diff --git a/user_guide_src/source/database/metadata.rst b/user_guide_src/source/database/metadata.rst index 41aa41ef9784..101d0a605683 100644 --- a/user_guide_src/source/database/metadata.rst +++ b/user_guide_src/source/database/metadata.rst @@ -127,6 +127,8 @@ for each key associated with a table. SQLite3 returns a pseudo index named ``PRIMARY``. But it is a special index, and you can't use it in your SQL commands. +.. _metadata-getforeignkeydata: + $db->getForeignKeyData() ------------------------ @@ -136,6 +138,4 @@ Usage example: .. literalinclude:: metadata/009.php -The object fields may be unique to the database you are using. For instance, SQLite3 does -not return data on column names, but has the additional *sequence* field for compound -foreign key definitions. +Foreign keys use the naming convention ``tableprefix_table_column1_column2_foreign``. Oracle uses a slightly different suffix of ``_fk``. diff --git a/user_guide_src/source/database/metadata/009.php b/user_guide_src/source/database/metadata/009.php index c4878c5186b2..26b832b0e2b1 100644 --- a/user_guide_src/source/database/metadata/009.php +++ b/user_guide_src/source/database/metadata/009.php @@ -4,10 +4,14 @@ $keys = $db->getForeignKeyData('table_name'); -foreach ($keys as $key) { - echo $key->constraint_name; - echo $key->table_name; - echo $key->column_name; - echo $key->foreign_table_name; - echo $key->foreign_column_name; +foreach ($keys as $key => $object) { + echo $key === $object->constraint_name; + echo $object->constraint_name; + echo $object->table_name; + echo $object->column_name[0]; // array + echo $object->foreign_table_name; + echo $object->foreign_column_name[0]; // array + echo $object->on_delete; + echo $object->on_update; + echo $object->match; } diff --git a/user_guide_src/source/dbmgmt/forge.rst b/user_guide_src/source/dbmgmt/forge.rst index 9193df744292..ed37af0dfacb 100644 --- a/user_guide_src/source/dbmgmt/forge.rst +++ b/user_guide_src/source/dbmgmt/forge.rst @@ -179,10 +179,12 @@ you may add them directly in forge: .. literalinclude:: forge/012.php -You can specify the desired action for the "on delete" and "on update" properties of the constraint: +You can specify the desired action for the "on delete" and "on update" properties of the constraint as well as the name: .. literalinclude:: forge/013.php +.. note:: SQLite does not support the naming of foreign keys. CodeIgniter will refer to them by ``prefix_table_column_foreign``. + Creating a Table ================ @@ -323,13 +325,14 @@ Class Reference Adds a field to the set that will be used to create a table. Usage: See `Adding Fields`_. - .. php:method:: addForeignKey($fieldName, $tableName, $tableField[, $onUpdate = '', $onDelete = '']) + .. php:method:: addForeignKey($fieldName, $tableName, $tableField[, $onUpdate = '', $onDelete = '', $fkName = '']) :param string|string[] $fieldName: Name of a key field or an array of fields :param string $tableName: Name of a parent table :param string|string[] $tableField: Name of a parent table field or an array of fields :param string $onUpdate: Desired action for the "on update" :param string $onDelete: Desired action for the "on delete" + :param string $fkName: Name of foreign key. This does not work with SQLite :returns: \CodeIgniter\Database\Forge instance (method chaining) :rtype: \CodeIgniter\Database\Forge diff --git a/user_guide_src/source/dbmgmt/forge/012.php b/user_guide_src/source/dbmgmt/forge/012.php index dc6ccdb24ec5..7d07425c24f2 100644 --- a/user_guide_src/source/dbmgmt/forge/012.php +++ b/user_guide_src/source/dbmgmt/forge/012.php @@ -1,7 +1,7 @@ addForeignKey('users_id', 'users', 'id'); -// gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) +// gives CONSTRAINT `TABLENAME_users_id_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) $forge->addForeignKey(['users_id', 'users_name'], 'users', ['id', 'name']); -// gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`) +// gives CONSTRAINT `TABLENAME_users_id_foreign` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`) diff --git a/user_guide_src/source/dbmgmt/forge/013.php b/user_guide_src/source/dbmgmt/forge/013.php index 33aa485380ee..fc789da8ba9a 100644 --- a/user_guide_src/source/dbmgmt/forge/013.php +++ b/user_guide_src/source/dbmgmt/forge/013.php @@ -1,7 +1,7 @@ addForeignKey('users_id', 'users', 'id', 'CASCADE', 'CASCADE'); -// gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +$forge->addForeignKey('users_id', 'users', 'id', 'CASCADE', 'CASCADE', 'my_fk_name'); +// gives CONSTRAINT `my_fk_name` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE -$forge->addForeignKey(['users_id', 'users_name'], 'users', ['id', 'name'], 'CASCADE', 'CASCADE'); -// gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`) ON DELETE CASCADE ON UPDATE CASCADE +$forge->addForeignKey(['users_id', 'users_name'], 'users', ['id', 'name'], 'CASCADE', 'CASCADE', 'my_fk_name'); +// gives CONSTRAINT `my_fk_name` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`) ON DELETE CASCADE ON UPDATE CASCADE diff --git a/user_guide_src/source/installation/upgrade_430.rst b/user_guide_src/source/installation/upgrade_430.rst index 5d9aac504cdb..ad11016c7280 100644 --- a/user_guide_src/source/installation/upgrade_430.rst +++ b/user_guide_src/source/installation/upgrade_430.rst @@ -122,6 +122,32 @@ Interface Changes Some interfaces has been fixed. See :ref:`v430-interface-changes` for details. +Foreign Key Data +===================================================== + +- The data structure returned by ``BaseConnection::getForeignKeyData()`` has been changed. + You will need to adjust any code depending on this method to use the new structure. + +Example: ``tableprefix_table_column1_column2_foreign`` + +The data returned has the following structure:: + + /** + * @return array[ + * {constraint_name} => + * stdClass[ + * 'constraint_name' => string, + * 'table_name' => string, + * 'column_name' => string[], + * 'foreign_table_name' => string, + * 'foreign_column_name' => string[], + * 'on_delete' => string, + * 'on_update' => string, + * 'match' => string + * ] + * ] + */ + Others ======