From 7f55e1b2bc64ea947d61523972a5fc9ec74836e3 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 10 May 2020 14:55:03 +0000 Subject: [PATCH] We now support real foreign keys --- src/Database/DBStructure.php | 107 +++++++++++++++++++++++++++++++++- static/dbstructure.config.php | 50 +++++++++------- update.php | 26 +++++++++ 3 files changed, 159 insertions(+), 24 deletions(-) diff --git a/src/Database/DBStructure.php b/src/Database/DBStructure.php index dc13bd656..5e5d71be8 100644 --- a/src/Database/DBStructure.php +++ b/src/Database/DBStructure.php @@ -162,11 +162,16 @@ class DBStructure $comment = ""; $sql_rows = []; $primary_keys = []; + $foreign_keys = []; + foreach ($structure["fields"] AS $fieldname => $field) { $sql_rows[] = "`" . DBA::escape($fieldname) . "` " . self::FieldCommand($field); if (!empty($field['primary'])) { $primary_keys[] = $fieldname; } + if (!empty($field['foreign'])) { + $foreign_keys[$fieldname] = $field; + } } if (!empty($structure["indexes"])) { @@ -178,6 +183,10 @@ class DBStructure } } + foreach ($foreign_keys AS $fieldname => $parameters) { + $sql_rows[] = self::foreignCommand($name, $fieldname, $parameters); + } + if (isset($structure["engine"])) { $engine = " ENGINE=" . $structure["engine"]; } @@ -295,7 +304,7 @@ class DBStructure $database = []; if (is_null($tables)) { - $tables = q("SHOW TABLES"); + $tables = DBA::toArray(DBA::p("SHOW TABLES")); } if (DBA::isResult($tables)) { @@ -387,6 +396,7 @@ class DBStructure // Remove the relation data that is used for the referential integrity unset($parameters['relation']); + unset($parameters['foreign']); // We change the collation after the indexes had been changed. // This is done to avoid index length problems. @@ -441,6 +451,40 @@ class DBStructure } } + $existing_foreign_keys = $database[$name]['foreign_keys']; + + // Foreign keys + // Compare the field structure field by field + foreach ($structure["fields"] AS $fieldname => $parameters) { + if (empty($parameters['foreign'])) { + continue; + } + + $constraint = self::getConstraintName($name, $fieldname, $parameters); + + unset($existing_foreign_keys[$constraint]); + + if (empty($database[$name]['foreign_keys'][$constraint])) { + $sql2 = self::addForeignKey($name, $fieldname, $parameters); + + if ($sql3 == "") { + $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2; + } else { + $sql3 .= ", " . $sql2; + } + } + } + + foreach ($existing_foreign_keys as $constraint => $param) { + $sql2 = self::dropForeignKey($constraint); + + if ($sql3 == "") { + $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2; + } else { + $sql3 .= ", " . $sql2; + } + } + if (isset($database[$name]["table_status"]["Comment"])) { $structurecomment = $structure["comment"] ?? ''; if ($database[$name]["table_status"]["Comment"] != $structurecomment) { @@ -596,7 +640,7 @@ class DBStructure } } - View::create($verbose, $action); + View::create(false, $action); if ($action && !$install) { DI::config()->set('system', 'maintenance', 0); @@ -620,6 +664,11 @@ class DBStructure $indexes = q("SHOW INDEX FROM `%s`", $table); + $foreign_keys = DBA::selectToArray(['INFORMATION_SCHEMA' => 'KEY_COLUMN_USAGE'], + ['COLUMN_NAME', 'CONSTRAINT_NAME', 'REFERENCED_TABLE_NAME', 'REFERENCED_COLUMN_NAME'], + ["`TABLE_SCHEMA` = ? AND `TABLE_NAME` = ? AND `REFERENCED_TABLE_SCHEMA` IS NOT NULL", + DBA::databaseName(), $table]); + $table_status = q("SHOW TABLE STATUS WHERE `name` = '%s'", $table); if (DBA::isResult($table_status)) { @@ -630,6 +679,15 @@ class DBStructure $fielddata = []; $indexdata = []; + $foreigndata = []; + + if (DBA::isResult($foreign_keys)) { + foreach ($foreign_keys as $foreign_key) { + $constraint = $foreign_key['CONSTRAINT_NAME']; + unset($foreign_key['CONSTRAINT_NAME']); + $foreigndata[$constraint] = $foreign_key; + } + } if (DBA::isResult($indexes)) { foreach ($indexes AS $index) { @@ -682,7 +740,8 @@ class DBStructure } } - return ["fields" => $fielddata, "indexes" => $indexdata, "table_status" => $table_status]; + return ["fields" => $fielddata, "indexes" => $indexdata, + "foreign_keys" => $foreigndata, "table_status" => $table_status]; } private static function dropIndex($indexname) @@ -703,6 +762,48 @@ class DBStructure return ($sql); } + private static function getConstraintName(string $tablename, string $fieldname, array $parameters) + { + $foreign_table = array_keys($parameters['foreign'])[0]; + $foreign_field = array_values($parameters['foreign'])[0]; + + return $tablename . "-" . $fieldname. "-" . $foreign_table. "-" . $foreign_field; + } + + private static function foreignCommand(string $tablename, string $fieldname, array $parameters) { + $foreign_table = array_keys($parameters['foreign'])[0]; + $foreign_field = array_values($parameters['foreign'])[0]; + + $constraint = self::getConstraintName($tablename, $fieldname, $parameters); + + $sql = "CONSTRAINT `" . $constraint . "` FOREIGN KEY (`" . $fieldname . "`)" . + " REFERENCES `" . $foreign_table . "` (`" . $foreign_field . "`)"; + + if (!empty($parameters['foreign']['on update'])) { + $sql .= " ON UPDATE " . strtoupper($parameters['foreign']['on update']); + } else { + $sql .= " ON UPDATE RESTRICT"; + } + + if (!empty($parameters['foreign']['on delete'])) { + $sql .= " ON DELETE " . strtoupper($parameters['foreign']['on delete']); + } else { + $sql .= " ON DELETE CASCADE"; + } + + return $sql; + } + + private static function addForeignKey(string $tablename, string $fieldname, array $parameters) + { + return sprintf("ADD %s", self::foreignCommand($tablename, $fieldname, $parameters)); + } + + private static function dropForeignKey(string $constraint) + { + return sprintf("DROP FOREIGN KEY `%s`", $constraint); + } + /** * Constructs a GROUP BY clause from a UNIQUE index definition. * diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 603327cd0..4d061452b 100755 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -32,7 +32,7 @@ * {"default" => "",} * {"default" => NULL_DATE,} (for datetime fields) * {"primary" => "1",} - * {"relation" => ["" => ""],} + * {"foreign|relation" => ["" => ""],} * "comment" => "Description of the fields" * ], * ... @@ -44,6 +44,9 @@ * ], * ], * + * Whenever possible prefer "foreign" before "relation" with the foreign keys. + * "foreign" adds true foreign keys on the database level, while "relation" simulates this behaviour. + * * If you need to make any change, make sure to increment the DB_UPDATE_VERSION constant value below. * */ @@ -51,7 +54,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1347); + define('DB_UPDATE_VERSION', 1348); } return [ @@ -158,7 +161,7 @@ return [ "comment" => "OAuth usage", "fields" => [ "id" => ["type" => "varchar(40)", "not null" => "1", "primary" => "1", "comment" => ""], - "client_id" => ["type" => "varchar(20)", "not null" => "1", "default" => "", "relation" => ["clients" => "client_id"], + "client_id" => ["type" => "varchar(20)", "not null" => "1", "default" => "", "foreign" => ["clients" => "client_id"], "comment" => ""], "redirect_uri" => ["type" => "varchar(200)", "not null" => "1", "default" => "", "comment" => ""], "expires" => ["type" => "int", "not null" => "1", "default" => "0", "comment" => ""], @@ -166,6 +169,7 @@ return [ ], "indexes" => [ "PRIMARY" => ["id"], + "client_id" => ["client_id"] ] ], "cache" => [ @@ -367,7 +371,7 @@ return [ "diaspora-interaction" => [ "comment" => "Signed Diaspora Interaction", "fields" => [ - "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "relation" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], "interaction" => ["type" => "mediumtext", "comment" => "The Diaspora interaction"] ], "indexes" => [ @@ -652,13 +656,13 @@ return [ "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "relation" => ["thread" => "iid"]], "guid" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "A unique identifier for this item"], "uri" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], - "uri-id" => ["type" => "int unsigned", "relation" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], "uri-hash" => ["type" => "varchar(80)", "not null" => "1", "default" => "", "comment" => "RIPEMD-128 hash from uri"], "parent" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "relation" => ["item" => "id"], "comment" => "item.id of the parent to this item if it is a reply of some form; otherwise this must be set to the id of this item"], "parent-uri" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "uri of the parent to this item"], - "parent-uri-id" => ["type" => "int unsigned", "relation" => ["item-uri" => "id"], "comment" => "Id of the item-uri table that contains the parent uri"], + "parent-uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table that contains the parent uri"], "thr-parent" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "If the parent of this item is not the top-level item in the conversation, the uri of the immediate parent; otherwise set to parent-uri"], - "thr-parent-id" => ["type" => "int unsigned", "relation" => ["item-uri" => "id"], "comment" => "Id of the item-uri table that contains the thread parent uri"], + "thr-parent-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table that contains the thread parent uri"], "created" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Creation timestamp."], "edited" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date of last edit (default is created)"], "commented" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date of last comment/reply to this item"], @@ -756,6 +760,8 @@ return [ "iaid" => ["iaid"], "psid_wall" => ["psid", "wall"], "uri-id" => ["uri-id"], + "parent-uri-id" => ["parent-uri-id"], + "thr-parent-id" => ["thr-parent-id"], ] ], "item-activity" => [ @@ -763,7 +769,7 @@ return [ "fields" => [ "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1"], "uri" => ["type" => "varchar(255)", "comment" => ""], - "uri-id" => ["type" => "int unsigned", "relation" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], "uri-hash" => ["type" => "varchar(80)", "not null" => "1", "default" => "", "comment" => "RIPEMD-128 hash from uri"], "activity" => ["type" => "smallint unsigned", "not null" => "1", "default" => "0", "comment" => ""] ], @@ -779,7 +785,7 @@ return [ "fields" => [ "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1"], "uri" => ["type" => "varchar(255)", "comment" => ""], - "uri-id" => ["type" => "int unsigned", "relation" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], "uri-plink-hash" => ["type" => "varchar(80)", "not null" => "1", "default" => "", "comment" => "RIPEMD-128 hash from uri"], "title" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "item title"], "content-warning" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], @@ -930,13 +936,14 @@ return [ "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"], "notify-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "relation" => ["notify" => "id"], "comment" => ""], "master-parent-item" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "relation" => ["item" => "id"], "comment" => ""], - "master-parent-uri-id" => ["type" => "int unsigned", "relation" => ["item-uri" => "id"], "comment" => "Item-uri id of the parent of the related post"], + "master-parent-uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Item-uri id of the parent of the related post"], "parent-item" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "comment" => ""], "receiver-uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "relation" => ["user" => "uid"], "comment" => "User id"], ], "indexes" => [ "PRIMARY" => ["id"], + "master-parent-uri-id" => ["master-parent-uri-id"], ] ], "oembed" => [ @@ -1293,10 +1300,10 @@ return [ "post-category" => [ "comment" => "post relation to categories", "fields" => [ - "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "relation" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], - "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "relation" => ["user" => "uid"], "comment" => "User id"], + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "primary" => "1", "relation" => ["user" => "uid"], "comment" => "User id"], "type" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "primary" => "1", "comment" => ""], - "tid" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "primary" => "1", "relation" => ["tag" => "id"], "comment" => ""], + "tid" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "primary" => "1", "foreign" => ["tag" => "id", "on delete" => "restrict"], "comment" => ""], ], "indexes" => [ "PRIMARY" => ["uri-id", "uid", "type", "tid"], @@ -1306,7 +1313,7 @@ return [ "post-delivery-data" => [ "comment" => "Delivery data for items", "fields" => [ - "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "relation" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], "postopts" => ["type" => "text", "comment" => "External post connectors add their network name to this comma-separated string to identify that they should be delivered to these networks during delivery"], "inform" => ["type" => "mediumtext", "comment" => "Additional receivers of the linked item"], "queue_count" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Initial number of delivery recipients, used as item.delivery_queue_count"], @@ -1325,15 +1332,15 @@ return [ "post-tag" => [ "comment" => "post relation to tags", "fields" => [ - "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "relation" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], "type" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "primary" => "1", "comment" => ""], - "tid" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "primary" => "1", "relation" => ["tag" => "id"], "comment" => ""], - "cid" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "primary" => "1", "relation" => ["contact" => "id"], "comment" => "Contact id of the mentioned public contact"], + "tid" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "primary" => "1", "foreign" => ["tag" => "id", "on delete" => "restrict"], "comment" => ""], + "cid" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "primary" => "1", "foreign" => ["contact" => "id", "on delete" => "restrict"], "comment" => "Contact id of the mentioned public contact"], ], "indexes" => [ "PRIMARY" => ["uri-id", "type", "tid", "cid"], - "uri-id" => ["tid"], - "cid" => ["tid"] + "tid" => ["tid"], + "cid" => ["cid"] ] ], "thread" => [ @@ -1386,13 +1393,14 @@ return [ "fields" => [ "id" => ["type" => "varchar(40)", "not null" => "1", "primary" => "1", "comment" => ""], "secret" => ["type" => "text", "comment" => ""], - "client_id" => ["type" => "varchar(20)", "not null" => "1", "default" => "", "relation" => ["clients" => "client_id"]], + "client_id" => ["type" => "varchar(20)", "not null" => "1", "default" => "", "foreign" => ["clients" => "client_id"]], "expires" => ["type" => "int", "not null" => "1", "default" => "0", "comment" => ""], "scope" => ["type" => "varchar(200)", "not null" => "1", "default" => "", "comment" => ""], "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "relation" => ["user" => "uid"], "comment" => "User id"], ], "indexes" => [ "PRIMARY" => ["id"], + "client_id" => ["client_id"] ] ], "user" => [ @@ -1493,7 +1501,7 @@ return [ "verb" => [ "comment" => "Activity Verbs", "fields" => [ - "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1"], + "id" => ["type" => "smallint unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1"], "name" => ["type" => "varchar(100)", "not null" => "1", "default" => "", "comment" => ""] ], "indexes" => [ diff --git a/update.php b/update.php index b7575e6c3..be1890b70 100644 --- a/update.php +++ b/update.php @@ -431,3 +431,29 @@ function update_1347() return Update::SUCCESS; } + +function pre_update_1348() +{ + DBA::insert('contact', ['nurl' => '']); + DBA::update('contact', ['id' => 0], ['id' => DBA::lastInsertId()]); + + // The tables "permissionset" and "tag" could or could not exist during the update. + // This depends upon the previous version. Depending upon this situation we have to add + // the "0" values before adding the foreign keys - or after would be sufficient. + + update_1348(); +} + +function update_1348() +{ + // Insert a permissionset with id=0 + // Setting it to -1 and then changing the value to 0 tricks the auto increment + DBA::insert('permissionset', ['allow_cid' => '', 'allow_gid' => '', 'deny_cid' => '', 'deny_gid' => '']); + DBA::update('permissionset', ['id' => 0], ['id' => DBA::lastInsertId()]); + + DBA::insert('tag', ['name' => '']); + DBA::update('tag', ['id' => 0], ['id' => DBA::lastInsertId()]); + + // to-do: Tag / contact + return Update::SUCCESS; +}