]> git.mxchange.org Git - friendica.git/blob - src/Database/DBStructure.php
bump version 2023.12
[friendica.git] / src / Database / DBStructure.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2023, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Database;
23
24 use Exception;
25 use Friendica\Core\Logger;
26 use Friendica\DI;
27 use Friendica\Model\Item;
28 use Friendica\Model\User;
29 use Friendica\Util\DateTimeFormat;
30 use Friendica\Util\Writer\DbaDefinitionSqlWriter;
31
32 /**
33  * This class contains functions that doesn't need to know if pdo, mysqli or whatever is used.
34  */
35 class DBStructure
36 {
37         const UPDATE_NOT_CHECKED = 0; // Database check wasn't executed before
38         const UPDATE_SUCCESSFUL  = 1; // Database check was successful
39         const UPDATE_FAILED      = 2; // Database check failed
40
41         const RENAME_COLUMN      = 0;
42         const RENAME_PRIMARY_KEY = 1;
43
44         /**
45          * Set a database version to trigger update functions
46          *
47          * @param string $version
48          * @return void
49          */
50         public static function setDatabaseVersion(string $version)
51         {
52                 if (!is_numeric($version)) {
53                         throw new \Asika\SimpleConsole\CommandArgsException('The version number must be numeric');
54                 }
55
56                 DI::keyValue()->set('build', $version);
57                 echo DI::l10n()->t('The database version had been set to %s.', $version);
58         }
59
60         /**
61          * Drops a specific table
62          *
63          * @param string $table the table name
64          *
65          * @return bool true if possible, otherwise false
66          */
67         public static function dropTable(string $table): bool
68         {
69                 return DBA::isResult(DBA::e('DROP TABLE ' . DBA::quoteIdentifier($table) . ';'));
70         }
71
72         /**
73          * Drop unused tables
74          *
75          * @param boolean $execute
76          * @return void
77          */
78         public static function dropTables(bool $execute)
79         {
80                 $postupdate = DI::keyValue()->get('post_update_version') ?? PostUpdate::VERSION;
81                 if ($postupdate < PostUpdate::VERSION) {
82                         echo DI::l10n()->t('The post update is at version %d, it has to be at %d to safely drop the tables.', $postupdate, PostUpdate::VERSION);
83                         return;
84                 }
85
86                 $old_tables = ['fserver', 'gcign', 'gcontact', 'gcontact-relation', 'gfollower' ,'glink', 'item-delivery-data',
87                         'item-activity', 'item-content', 'item_id', 'participation', 'poll', 'poll_result', 'queue', 'retriever_rule',
88                         'deliverq', 'dsprphotoq', 'ffinder', 'sign', 'spam', 'term', 'user-item', 'thread', 'item', 'challenge',
89                         'auth_codes', 'tokens', 'clients', 'profile_check', 'host', 'conversation', 'fcontact', 'addon'];
90
91                 $tables = DBA::selectToArray('INFORMATION_SCHEMA.TABLES', ['TABLE_NAME'],
92                         ['TABLE_SCHEMA' => DBA::databaseName(), 'TABLE_TYPE' => 'BASE TABLE']);
93
94                 if (empty($tables)) {
95                         echo DI::l10n()->t('No unused tables found.');
96                         return;
97                 }
98
99                 if (!$execute) {
100                         echo DI::l10n()->t('These tables are not used for friendica and will be deleted when you execute "dbstructure drop -e":') . "\n\n";
101                 }
102
103                 foreach ($old_tables as $table) {
104                         if (in_array($table, array_column($tables, 'TABLE_NAME'))) {
105                                 if ($execute) {
106                                         $sql = 'DROP TABLE ' . DBA::quoteIdentifier($table) . ';';
107                                         echo $sql . "\n";
108
109                                         if (!static::dropTable($table)) {
110                                                 self::printUpdateError($sql);
111                                         }
112                                 } else {
113                                         echo $table . "\n";
114                                 }
115                         }
116                 }
117         }
118
119         /**
120          * Converts all tables from MyISAM/InnoDB Antelope to InnoDB Barracuda
121          */
122         public static function convertToInnoDB()
123         {
124                 $tables = DBA::selectToArray(
125                         'information_schema.tables',
126                         ['table_name'],
127                         ['engine' => 'MyISAM', 'table_schema' => DBA::databaseName()]
128                 );
129
130                 $tables = array_merge($tables, DBA::selectToArray(
131                         'information_schema.tables',
132                         ['table_name'],
133                         ['engine' => 'InnoDB', 'ROW_FORMAT' => ['COMPACT', 'REDUNDANT'], 'table_schema' => DBA::databaseName()]
134                 ));
135
136                 if (!DBA::isResult($tables)) {
137                         echo DI::l10n()->t('There are no tables on MyISAM or InnoDB with the Antelope file format.') . "\n";
138                         return;
139                 }
140
141                 foreach ($tables as $table) {
142                         $sql = "ALTER TABLE " . DBA::quoteIdentifier($table['table_name']) . " ENGINE=InnoDB ROW_FORMAT=DYNAMIC;";
143                         echo $sql . "\n";
144
145                         $result = DBA::e($sql);
146                         if (!DBA::isResult($result)) {
147                                 self::printUpdateError($sql);
148                         }
149                 }
150         }
151
152         /**
153          * Print out database error messages
154          *
155          * @param string $message Message to be added to the error message
156          *
157          * @return string Error message
158          */
159         private static function printUpdateError(string $message): string
160         {
161                 echo DI::l10n()->t("\nError %d occurred during database update:\n%s\n",
162                         DBA::errorNo(), DBA::errorMessage());
163
164                 return DI::l10n()->t('Errors encountered performing database changes: ') . $message . '<br />';
165         }
166
167         /**
168          * Perform a database structure dryrun (means: just simulating)
169          *
170          * @return string Empty string if the update is successful, error messages otherwise
171          * @throws Exception
172          */
173         public static function dryRun(): string
174         {
175                 return self::update(true, false);
176         }
177
178         /**
179          * Updates DB structure and returns eventual errors messages
180          *
181          * @param bool $enable_maintenance_mode Set the maintenance mode
182          * @param bool $verbose                 Display the SQL commands
183          *
184          * @return string Empty string if the update is successful, error messages otherwise
185          * @throws Exception
186          */
187         public static function performUpdate(bool $enable_maintenance_mode = true, bool $verbose = false): string
188         {
189                 if ($enable_maintenance_mode) {
190                         DI::config()->set('system', 'maintenance', true);
191                 }
192
193                 $status = self::update($verbose, true);
194
195                 if ($enable_maintenance_mode) {
196                         DI::config()->beginTransaction()
197                                                 ->set('system', 'maintenance', false)
198                                                 ->delete('system', 'maintenance_reason')
199                                                 ->commit();
200                 }
201
202                 return $status;
203         }
204
205         /**
206          * Updates DB structure from the installation and returns eventual errors messages
207          *
208          * @return string Empty string if the update is successful, error messages otherwise
209          * @throws Exception
210          */
211         public static function install(): string
212         {
213                 return self::update(false, true, true);
214         }
215
216         /**
217          * Updates DB structure and returns eventual errors messages
218          *
219          * @param bool   $verbose
220          * @param bool   $action     Whether to actually apply the update
221          * @param bool   $install    Is this the initial update during the installation?
222          * @param array  $tables     An array of the database tables
223          * @param array  $definition An array of the definition tables
224          * @return string Empty string if the update is successful, error messages otherwise
225          * @throws Exception
226          */
227         private static function update(bool $verbose, bool $action, bool $install = false, array $tables = null, array $definition = null): string
228         {
229                 $in_maintenance_mode = DI::config()->get('system', 'maintenance');
230
231                 if ($action && !$install && self::isUpdating()) {
232                         return DI::l10n()->t('Another database update is currently running.');
233                 }
234
235                 if ($in_maintenance_mode) {
236                         DI::config()->set('system', 'maintenance_reason', DI::l10n()->t('%s: Database update', DateTimeFormat::utcNow() . ' ' . date('e')));
237                 }
238
239                 // ensure that all initial values exist. This test has to be done prior and after the structure check.
240                 // Prior is needed if the specific tables already exists - after is needed when they had been created.
241                 self::checkInitialValues();
242
243                 $errors = '';
244
245                 Logger::info('updating structure');
246
247                 // Get the current structure
248                 $database = [];
249
250                 if (is_null($tables)) {
251                         $tables = DBA::toArray(DBA::p("SHOW TABLES"));
252                 }
253
254                 if (DBA::isResult($tables)) {
255                         foreach ($tables as $table) {
256                                 $table = current($table);
257
258                                 Logger::info('updating structure', ['table' => $table]);
259                                 $database[$table] = self::tableStructure($table);
260                         }
261                 }
262
263                 // Get the definition
264                 if (is_null($definition)) {
265                         // just for Update purpose, reload the DBA definition with addons to explicit get the whole definition
266                         $definition = DI::dbaDefinition()->load(true)->getAll();
267                 }
268
269                 // MySQL >= 5.7.4 doesn't support the IGNORE keyword in ALTER TABLE statements
270                 if ((version_compare(DBA::serverInfo(), '5.7.4') >= 0) &&
271                         !(strpos(DBA::serverInfo(), 'MariaDB') !== false)) {
272                         $ignore = '';
273                 } else {
274                         $ignore = ' IGNORE';
275                 }
276
277                 // Compare it
278                 foreach ($definition as $name => $structure) {
279                         $is_new_table = false;
280                         $sql3         = "";
281                         if (!isset($database[$name])) {
282                                 $sql = DbaDefinitionSqlWriter::createTable($name, $structure, $verbose, $action);
283                                 if ($verbose) {
284                                         echo $sql;
285                                 }
286                                 if ($action) {
287                                         $r = DBA::e($sql);
288                                         if (!DBA::isResult($r)) {
289                                                 $errors .= self::printUpdateError($name);
290                                         }
291                                 }
292                                 $is_new_table = true;
293                         } else {
294                                 /*
295                                  * Drop the index if it isn't present in the definition
296                                  * or the definition differ from current status
297                                  * and index name doesn't start with "local_"
298                                  */
299                                 foreach ($database[$name]["indexes"] as $indexName => $fieldNames) {
300                                         $current_index_definition = implode(",", $fieldNames);
301                                         if (isset($structure["indexes"][$indexName])) {
302                                                 $new_index_definition = implode(",", $structure["indexes"][$indexName]);
303                                         } else {
304                                                 $new_index_definition = "__NOT_SET__";
305                                         }
306                                         if ($current_index_definition != $new_index_definition && substr($indexName, 0, 6) != 'local_') {
307                                                 $sql2 = DbaDefinitionSqlWriter::dropIndex($indexName);
308                                                 if ($sql3 == "") {
309                                                         $sql3 = "ALTER" . $ignore . " TABLE `" . $name . "` " . $sql2;
310                                                 } else {
311                                                         $sql3 .= ", " . $sql2;
312                                                 }
313                                         }
314                                 }
315                                 // Compare the field structure field by field
316                                 foreach ($structure["fields"] as $fieldName => $parameters) {
317                                         if (!isset($database[$name]["fields"][$fieldName])) {
318                                                 $sql2 = DbaDefinitionSqlWriter::addTableField($fieldName, $parameters);
319                                                 if ($sql3 == "") {
320                                                         $sql3 = "ALTER" . $ignore . " TABLE `" . $name . "` " . $sql2;
321                                                 } else {
322                                                         $sql3 .= ", " . $sql2;
323                                                 }
324                                         } else {
325                                                 // Compare the field definition
326                                                 $field_definition = $database[$name]["fields"][$fieldName];
327
328                                                 // Remove the relation data that is used for the referential integrity
329                                                 unset($parameters['relation']);
330                                                 unset($parameters['foreign']);
331
332                                                 // We change the collation after the indexes had been changed.
333                                                 // This is done to avoid index length problems.
334                                                 // So here we always ensure that there is no need to change it.
335                                                 unset($parameters['Collation']);
336                                                 unset($field_definition['Collation']);
337
338                                                 // Only update the comment when it is defined
339                                                 if (!isset($parameters['comment'])) {
340                                                         $parameters['comment'] = "";
341                                                 }
342
343                                                 $current_field_definition = DBA::cleanQuery(implode(",", $field_definition));
344                                                 $new_field_definition     = DBA::cleanQuery(implode(",", $parameters));
345                                                 if ($current_field_definition != $new_field_definition) {
346                                                         $sql2 = DbaDefinitionSqlWriter::modifyTableField($fieldName, $parameters);
347                                                         if ($sql3 == "") {
348                                                                 $sql3 = "ALTER" . $ignore . " TABLE `" . $name . "` " . $sql2;
349                                                         } else {
350                                                                 $sql3 .= ", " . $sql2;
351                                                         }
352                                                 }
353                                         }
354                                 }
355                         }
356
357                         /*
358                          * Create the index if the index don't exists in database
359                          * or the definition differ from the current status.
360                          * Don't create keys if table is new
361                          */
362                         if (!$is_new_table) {
363                                 foreach ($structure["indexes"] as $indexName => $fieldNames) {
364                                         if (isset($database[$name]["indexes"][$indexName])) {
365                                                 $current_index_definition = implode(",", $database[$name]["indexes"][$indexName]);
366                                         } else {
367                                                 $current_index_definition = "__NOT_SET__";
368                                         }
369                                         $new_index_definition = implode(",", $fieldNames);
370                                         if ($current_index_definition != $new_index_definition) {
371                                                 $sql2 = DbaDefinitionSqlWriter::createIndex($indexName, $fieldNames);
372
373                                                 if ($sql2 != "") {
374                                                         if ($sql3 == "") {
375                                                                 $sql3 = "ALTER" . $ignore . " TABLE `" . $name . "` " . $sql2;
376                                                         } else {
377                                                                 $sql3 .= ", " . $sql2;
378                                                         }
379                                                 }
380                                         }
381                                 }
382
383                                 $existing_foreign_keys = $database[$name]['foreign_keys'];
384
385                                 // Foreign keys
386                                 // Compare the field structure field by field
387                                 foreach ($structure["fields"] as $fieldName => $parameters) {
388                                         if (empty($parameters['foreign'])) {
389                                                 continue;
390                                         }
391
392                                         $constraint = self::getConstraintName($name, $fieldName, $parameters);
393
394                                         unset($existing_foreign_keys[$constraint]);
395
396                                         if (empty($database[$name]['foreign_keys'][$constraint])) {
397                                                 $sql2 = DbaDefinitionSqlWriter::addForeignKey($fieldName, $parameters);
398
399                                                 if ($sql3 == "") {
400                                                         $sql3 = "ALTER" . $ignore . " TABLE `" . $name . "` " . $sql2;
401                                                 } else {
402                                                         $sql3 .= ", " . $sql2;
403                                                 }
404                                         }
405                                 }
406
407                                 foreach ($existing_foreign_keys as $param) {
408                                         $sql2 = DbaDefinitionSqlWriter::dropForeignKey($param['CONSTRAINT_NAME']);
409
410                                         if ($sql3 == "") {
411                                                 $sql3 = "ALTER" . $ignore . " TABLE `" . $name . "` " . $sql2;
412                                         } else {
413                                                 $sql3 .= ", " . $sql2;
414                                         }
415                                 }
416
417                                 if (isset($database[$name]["table_status"]["TABLE_COMMENT"])) {
418                                         $structurecomment = $structure["comment"] ?? '';
419                                         if ($database[$name]["table_status"]["TABLE_COMMENT"] != $structurecomment) {
420                                                 $sql2 = "COMMENT = '" . DBA::escape($structurecomment) . "'";
421
422                                                 if ($sql3 == "") {
423                                                         $sql3 = "ALTER" . $ignore . " TABLE `" . $name . "` " . $sql2;
424                                                 } else {
425                                                         $sql3 .= ", " . $sql2;
426                                                 }
427                                         }
428                                 }
429
430                                 if (isset($database[$name]["table_status"]["ENGINE"]) && isset($structure['engine'])) {
431                                         if ($database[$name]["table_status"]["ENGINE"] != $structure['engine']) {
432                                                 $sql2 = "ENGINE = '" . DBA::escape($structure['engine']) . "'";
433
434                                                 if ($sql3 == "") {
435                                                         $sql3 = "ALTER" . $ignore . " TABLE `" . $name . "` " . $sql2;
436                                                 } else {
437                                                         $sql3 .= ", " . $sql2;
438                                                 }
439                                         }
440                                 }
441
442                                 if (isset($database[$name]["table_status"]["TABLE_COLLATION"])) {
443                                         if ($database[$name]["table_status"]["TABLE_COLLATION"] != 'utf8mb4_general_ci') {
444                                                 $sql2 = "DEFAULT COLLATE utf8mb4_general_ci";
445
446                                                 if ($sql3 == "") {
447                                                         $sql3 = "ALTER" . $ignore . " TABLE `" . $name . "` " . $sql2;
448                                                 } else {
449                                                         $sql3 .= ", " . $sql2;
450                                                 }
451                                         }
452                                 }
453
454                                 if ($sql3 != "") {
455                                         $sql3 .= "; ";
456                                 }
457
458                                 // Now have a look at the field collations
459                                 // Compare the field structure field by field
460                                 foreach ($structure["fields"] as $fieldName => $parameters) {
461                                         // Compare the field definition
462                                         $field_definition = ($database[$name]["fields"][$fieldName] ?? '') ?: ['Collation' => ''];
463
464                                         // Define the default collation if not given
465                                         if (!isset($parameters['Collation']) && !empty($field_definition['Collation'])) {
466                                                 $parameters['Collation'] = 'utf8mb4_general_ci';
467                                         } else {
468                                                 $parameters['Collation'] = null;
469                                         }
470
471                                         if ($field_definition['Collation'] != $parameters['Collation']) {
472                                                 $sql2 = DbaDefinitionSqlWriter::modifyTableField($fieldName, $parameters);
473                                                 if (($sql3 == "") || (substr($sql3, -2, 2) == "; ")) {
474                                                         $sql3 .= "ALTER" . $ignore . " TABLE `" . $name . "` " . $sql2;
475                                                 } else {
476                                                         $sql3 .= ", " . $sql2;
477                                                 }
478                                         }
479                                 }
480                         }
481
482                         if ($sql3 != "") {
483                                 if (substr($sql3, -2, 2) != "; ") {
484                                         $sql3 .= ";";
485                                 }
486
487                                 if ($verbose) {
488                                         echo $sql3 . "\n";
489                                 }
490
491                                 if ($action) {
492                                         if ($in_maintenance_mode) {
493                                                 DI::config()->set('system', 'maintenance_reason', DI::l10n()->t('%s: updating %s table.', DateTimeFormat::utcNow() . ' ' . date('e'), $name));
494                                         }
495
496                                         $r = DBA::e($sql3);
497                                         if (!DBA::isResult($r)) {
498                                                 $errors .= self::printUpdateError($sql3);
499                                         }
500                                 }
501                         }
502                 }
503
504                 View::create(false, $action);
505
506                 self::checkInitialValues();
507
508                 if ($action && !$install) {
509                         if ($errors) {
510                                 DI::config()->set('system', 'dbupdate', self::UPDATE_FAILED);
511                         } else {
512                                 DI::config()->set('system', 'dbupdate', self::UPDATE_SUCCESSFUL);
513                         }
514                 }
515
516                 return $errors;
517         }
518
519         /**
520          * Returns an array with table structure information
521          *
522          * @param string $table Name of table
523          * @return array Table structure information
524          */
525         private static function tableStructure(string $table): array
526         {
527                 // This query doesn't seem to be executable as a prepared statement
528                 $indexes = DBA::toArray(DBA::p("SHOW INDEX FROM " . DBA::quoteIdentifier($table)));
529
530                 $fields = DBA::selectToArray('INFORMATION_SCHEMA.COLUMNS',
531                         ['COLUMN_NAME', 'COLUMN_TYPE', 'IS_NULLABLE', 'COLUMN_DEFAULT', 'EXTRA',
532                         'COLUMN_KEY', 'COLLATION_NAME', 'COLUMN_COMMENT'],
533                         ["`TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?",
534                         DBA::databaseName(), $table]);
535
536                 $foreign_keys = DBA::selectToArray('INFORMATION_SCHEMA.KEY_COLUMN_USAGE',
537                         ['COLUMN_NAME', 'CONSTRAINT_NAME', 'REFERENCED_TABLE_NAME', 'REFERENCED_COLUMN_NAME'],
538                         ["`TABLE_SCHEMA` = ? AND `TABLE_NAME` = ? AND `REFERENCED_TABLE_SCHEMA` IS NOT NULL",
539                         DBA::databaseName(), $table]);
540
541                 $table_status = DBA::selectFirst('INFORMATION_SCHEMA.TABLES',
542                         ['ENGINE', 'TABLE_COLLATION', 'TABLE_COMMENT'],
543                         ["`TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?",
544                         DBA::databaseName(), $table]);
545
546                 $fielddata = [];
547                 $indexdata = [];
548                 $foreigndata = [];
549
550                 if (DBA::isResult($foreign_keys)) {
551                         foreach ($foreign_keys as $foreign_key) {
552                                 $parameters = ['foreign' => [$foreign_key['REFERENCED_TABLE_NAME'] => $foreign_key['REFERENCED_COLUMN_NAME']]];
553                                 $constraint = self::getConstraintName($table, $foreign_key['COLUMN_NAME'], $parameters);
554                                 $foreigndata[$constraint] = $foreign_key;
555                         }
556                 }
557
558                 if (DBA::isResult($indexes)) {
559                         foreach ($indexes as $index) {
560                                 if ($index["Key_name"] != "PRIMARY" && $index["Non_unique"] == "0" && !isset($indexdata[$index["Key_name"]])) {
561                                         $indexdata[$index["Key_name"]] = ["UNIQUE"];
562                                 }
563
564                                 if ($index["Index_type"] == "FULLTEXT" && !isset($indexdata[$index["Key_name"]])) {
565                                         $indexdata[$index["Key_name"]] = ["FULLTEXT"];
566                                 }
567
568                                 $column = $index["Column_name"];
569
570                                 if ($index["Sub_part"] != "") {
571                                         $column .= "(" . $index["Sub_part"] . ")";
572                                 }
573
574                                 $indexdata[$index["Key_name"]][] = $column;
575                         }
576                 }
577
578                 $fielddata = [];
579                 if (DBA::isResult($fields)) {
580                         foreach ($fields as $field) {
581                                 $search = ['tinyint(1)', 'tinyint(3) unsigned', 'tinyint(4)', 'smallint(5) unsigned', 'smallint(6)', 'mediumint(8) unsigned', 'mediumint(9)', 'bigint(20)', 'int(10) unsigned', 'int(11)'];
582                                 $replace = ['boolean', 'tinyint unsigned', 'tinyint', 'smallint unsigned', 'smallint', 'mediumint unsigned', 'mediumint', 'bigint', 'int unsigned', 'int'];
583                                 $field['COLUMN_TYPE'] = str_replace($search, $replace, $field['COLUMN_TYPE']);
584
585                                 $fielddata[$field['COLUMN_NAME']]['type'] = $field['COLUMN_TYPE'];
586
587                                 if ($field['IS_NULLABLE'] == 'NO') {
588                                         $fielddata[$field['COLUMN_NAME']]['not null'] = true;
589                                 }
590
591                                 if (isset($field['COLUMN_DEFAULT']) && ($field['COLUMN_DEFAULT'] != 'NULL')) {
592                                         $fielddata[$field['COLUMN_NAME']]['default'] = trim($field['COLUMN_DEFAULT'], "'");
593                                 }
594
595                                 if (!empty($field['EXTRA'])) {
596                                         $fielddata[$field['COLUMN_NAME']]['extra'] = $field['EXTRA'];
597                                 }
598
599                                 if ($field['COLUMN_KEY'] == 'PRI') {
600                                         $fielddata[$field['COLUMN_NAME']]['primary'] = true;
601                                 }
602
603                                 $fielddata[$field['COLUMN_NAME']]['Collation'] = $field['COLLATION_NAME'];
604                                 $fielddata[$field['COLUMN_NAME']]['comment'] = $field['COLUMN_COMMENT'];
605                         }
606                 }
607
608                 return [
609                         'fields' => $fielddata,
610                         'indexes' => $indexdata,
611                         'foreign_keys' => $foreigndata,
612                         'table_status' => $table_status
613                 ];
614         }
615
616         private static function getConstraintName(string $tableName, string $fieldName, array $parameters): string
617         {
618                 $foreign_table = array_keys($parameters['foreign'])[0];
619                 $foreign_field = array_values($parameters['foreign'])[0];
620
621                 return $tableName . '-' . $fieldName. '-' . $foreign_table. '-' . $foreign_field;
622         }
623
624         /**
625          * Renames columns or the primary key of a table
626          *
627          * @todo You cannot rename a primary key if "auto increment" is set
628          *
629          * @param string $table            Table name
630          * @param array  $columns          Columns Syntax for Rename: [ $old1 => [ $new1, $type1 ], $old2 => [ $new2, $type2 ], ... ]
631          *                                 Syntax for Primary Key: [ $col1, $col2, ...]
632          * @param int    $type             The type of renaming (Default is Column)
633          *
634          * @return boolean Was the renaming successful?
635          * @throws Exception
636          */
637         public static function rename(string $table, array $columns, int $type = self::RENAME_COLUMN): bool
638         {
639                 if (empty($table) || empty($columns)) {
640                         return false;
641                 }
642
643                 if (!is_array($columns)) {
644                         return false;
645                 }
646
647                 $table = DBA::escape($table);
648
649                 $sql = "ALTER TABLE `" . $table . "`";
650                 switch ($type) {
651                         case self::RENAME_COLUMN:
652                                 if (!self::existsColumn($table, array_keys($columns))) {
653                                         return false;
654                                 }
655                                 $sql .= implode(',', array_map(
656                                         function ($to, $from) {
657                                                 return " CHANGE `" . $from . "` `" . $to[0] . "` " . $to[1];
658                                         },
659                                         $columns,
660                                         array_keys($columns)
661                                 ));
662                                 break;
663                         case self::RENAME_PRIMARY_KEY:
664                                 if (!self::existsColumn($table, $columns)) {
665                                         return false;
666                                 }
667                                 $sql .= " DROP PRIMARY KEY, ADD PRIMARY KEY(`" . implode('`, `', $columns) . "`)";
668                                 break;
669                         default:
670                                 return false;
671                 }
672
673                 $sql .= ';';
674
675                 $stmt = DBA::p($sql);
676
677                 if (is_bool($stmt)) {
678                         $retval = $stmt;
679                 } else {
680                         $retval = true;
681                 }
682
683                 DBA::close($stmt);
684
685                 return $retval;
686         }
687
688         /**
689          *    Check if the columns of the table exists
690          *
691          * @param string $table   Table name
692          * @param array  $columns Columns to check ( Syntax: [ $col1, $col2, .. ] )
693          *
694          * @return boolean Does the table exist?
695          * @throws Exception
696          */
697         public static function existsColumn(string $table, array $columns = []): bool
698         {
699                 if (empty($table)) {
700                         return false;
701                 }
702
703                 if (is_null($columns) || empty($columns)) {
704                         return self::existsTable($table);
705                 }
706
707                 $table = DBA::escape($table);
708
709                 foreach ($columns as $column) {
710                         $sql = "SHOW COLUMNS FROM `" . $table . "` LIKE '" . $column . "';";
711
712                         $stmt = DBA::p($sql);
713
714                         if (is_bool($stmt)) {
715                                 $retval = $stmt;
716                         } else {
717                                 $retval = (DBA::numRows($stmt) > 0);
718                         }
719
720                         DBA::close($stmt);
721
722                         if (!$retval) {
723                                 return false;
724                         }
725                 }
726
727                 return true;
728         }
729
730         /**
731          * Check if a foreign key exists for the given table field
732          *
733          * @param string $table Table name
734          * @param string $field Field name
735          * @return boolean Wether a foreign key exists
736          */
737         public static function existsForeignKeyForField(string $table, string $field): bool
738         {
739                 return DBA::exists('INFORMATION_SCHEMA.KEY_COLUMN_USAGE',
740                         ["`TABLE_SCHEMA` = ? AND `TABLE_NAME` = ? AND `COLUMN_NAME` = ? AND `REFERENCED_TABLE_SCHEMA` IS NOT NULL",
741                         DBA::databaseName(), $table, $field]);
742         }
743
744         /**
745          * Check if a table exists
746          *
747          * @param string $table Single table name (please loop yourself)
748          * @return boolean Does the table exist?
749          * @throws Exception
750          */
751         public static function existsTable(string $table): bool
752         {
753                 if (empty($table)) {
754                         return false;
755                 }
756
757                 $condition = ['table_schema' => DBA::databaseName(), 'table_name' => $table];
758
759                 return DBA::exists('information_schema.tables', $condition);
760         }
761
762         /**
763          * Returns the columns of a table
764          *
765          * @param string $table Table name
766          *
767          * @return array An array of the table columns
768          * @throws Exception
769          */
770         public static function getColumns(string $table): array
771         {
772                 $stmtColumns = DBA::p("SHOW COLUMNS FROM `" . $table . "`");
773                 return DBA::toArray($stmtColumns);
774         }
775
776         /**
777          * Check if initial database values do exist - or create them
778          *
779          * @param bool $verbose Whether to output messages
780          * @return void
781          */
782         public static function checkInitialValues(bool $verbose = false)
783         {
784                 if (self::existsTable('verb')) {
785                         if (!DBA::exists('verb', ['id' => 1])) {
786                                 foreach (Item::ACTIVITIES as $index => $activity) {
787                                         DBA::insert('verb', ['id' => $index + 1, 'name' => $activity], Database::INSERT_IGNORE);
788                                 }
789                                 if ($verbose) {
790                                         echo "verb: activities added\n";
791                                 }
792                         } elseif ($verbose) {
793                                 echo "verb: activities already added\n";
794                         }
795
796                         if (!DBA::exists('verb', ['id' => 0])) {
797                                 DBA::insert('verb', ['name' => ''], Database::INSERT_IGNORE);
798                                 $lastid = DBA::lastInsertId();
799                                 if ($lastid != 0) {
800                                         DBA::update('verb', ['id' => 0], ['id' => $lastid]);
801                                         if ($verbose) {
802                                                 echo "Zero verb added\n";
803                                         }
804                                 }
805                         } elseif ($verbose) {
806                                 echo "Zero verb already added\n";
807                         }
808                 } elseif ($verbose) {
809                         echo "verb: Table not found\n";
810                 }
811
812                 if (self::existsTable('user') && !DBA::exists('user', ['uid' => 0])) {
813                         $user = [
814                                 'verified' => true,
815                                 'page-flags' => User::PAGE_FLAGS_SOAPBOX,
816                                 'account-type' => User::ACCOUNT_TYPE_RELAY,
817                         ];
818                         DBA::insert('user', $user);
819                         $lastid = DBA::lastInsertId();
820                         if ($lastid != 0) {
821                                 DBA::update('user', ['uid' => 0], ['uid' => $lastid]);
822                                 if ($verbose) {
823                                         echo "Zero user added\n";
824                                 }
825                         }
826                 } elseif (self::existsTable('user') && $verbose) {
827                         echo "Zero user already added\n";
828                 } elseif ($verbose) {
829                         echo "user: Table not found\n";
830                 }
831
832                 if (self::existsTable('contact') && !DBA::exists('contact', ['id' => 0])) {
833                         DBA::insert('contact', ['nurl' => ''], Database::INSERT_IGNORE);
834                         $lastid = DBA::lastInsertId();
835                         if ($lastid != 0) {
836                                 DBA::update('contact', ['id' => 0], ['id' => $lastid]);
837                                 if ($verbose) {
838                                         echo "Zero contact added\n";
839                                 }
840                         }
841                 } elseif (self::existsTable('contact') && $verbose) {
842                         echo "Zero contact already added\n";
843                 } elseif ($verbose) {
844                         echo "contact: Table not found\n";
845                 }
846
847                 if (self::existsTable('tag') && !DBA::exists('tag', ['id' => 0])) {
848                         DBA::insert('tag', ['name' => ''], Database::INSERT_IGNORE);
849                         $lastid = DBA::lastInsertId();
850                         if ($lastid != 0) {
851                                 DBA::update('tag', ['id' => 0], ['id' => $lastid]);
852                                 if ($verbose) {
853                                         echo "Zero tag added\n";
854                                 }
855                         }
856                 } elseif (self::existsTable('tag') && $verbose) {
857                         echo "Zero tag already added\n";
858                 } elseif ($verbose) {
859                         echo "tag: Table not found\n";
860                 }
861
862                 if (self::existsTable('permissionset')) {
863                         if (!DBA::exists('permissionset', ['id' => 0])) {
864                                 DBA::insert('permissionset', ['allow_cid' => '', 'allow_gid' => '', 'deny_cid' => '', 'deny_gid' => ''], Database::INSERT_IGNORE);
865                                 $lastid = DBA::lastInsertId();
866                                 if ($lastid != 0) {
867                                         DBA::update('permissionset', ['id' => 0], ['id' => $lastid]);
868                                         if ($verbose) {
869                                                 echo "Zero permissionset added\n";
870                                         }
871                                 }
872                         } elseif ($verbose) {
873                                 echo "Zero permissionset already added\n";
874                         }
875                         if (self::existsTable('item') && !self::existsForeignKeyForField('item', 'psid')) {
876                                 $sets = DBA::p("SELECT `psid`, `item`.`uid`, `item`.`private` FROM `item`
877                                         LEFT JOIN `permissionset` ON `permissionset`.`id` = `item`.`psid`
878                                         WHERE `permissionset`.`id` IS NULL AND NOT `psid` IS NULL");
879                                 while ($set = DBA::fetch($sets)) {
880                                         if (($set['private'] == Item::PRIVATE) && ($set['uid'] != 0)) {
881                                                 $owner = User::getOwnerDataById($set['uid']);
882                                                 if ($owner) {
883                                                         $permission = '<' . $owner['id'] . '>';
884                                                 } else {
885                                                         $permission = '<>';
886                                                 }
887                                         } else {
888                                                 $permission = '';
889                                         }
890                                         $fields = ['id' => $set['psid'], 'uid' => $set['uid'], 'allow_cid' => $permission,
891                                                 'allow_gid' => '', 'deny_cid' => '', 'deny_gid' => ''];
892                                         DBA::insert('permissionset', $fields, Database::INSERT_IGNORE);
893                                 }
894                                 DBA::close($sets);
895                         }
896                 } elseif ($verbose) {
897                         echo "permissionset: Table not found\n";
898                 }
899
900                 if (self::existsTable('tokens') && self::existsTable('clients') && !self::existsForeignKeyForField('tokens', 'client_id')) {
901                         $tokens = DBA::p("SELECT `tokens`.`id` FROM `tokens`
902                                 LEFT JOIN `clients` ON `clients`.`client_id` = `tokens`.`client_id`
903                                 WHERE `clients`.`client_id` IS NULL");
904                         while ($token = DBA::fetch($tokens)) {
905                                 DBA::delete('tokens', ['id' => $token['id']]);
906                         }
907                         DBA::close($tokens);
908                 }
909         }
910
911         /**
912          * Checks if a database update is currently running
913          *
914          * @return boolean
915          */
916         private static function isUpdating(): bool
917         {
918                 $isUpdate = false;
919
920                 $processes = DBA::select('information_schema.processlist', ['info'], [
921                         'db' => DBA::databaseName(),
922                         'command' => ['Query', 'Execute']
923                 ]);
924
925                 while ($process = DBA::fetch($processes)) {
926                         $parts = explode(' ', $process['info']);
927                         if (in_array(strtolower(array_shift($parts)), ['alter', 'create', 'drop', 'rename'])) {
928                                 $isUpdate = true;
929                         }
930                 }
931
932                 DBA::close($processes);
933
934                 return $isUpdate;
935         }
936 }