Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

842 lines
23KB

  1. <?php
  2. /**
  3. * @file src/Database/DBStructure.php
  4. */
  5. namespace Friendica\Database;
  6. use Exception;
  7. use Friendica\Core\Config;
  8. use Friendica\Core\Hook;
  9. use Friendica\Core\L10n;
  10. use Friendica\Core\Logger;
  11. use Friendica\Util\DateTimeFormat;
  12. /**
  13. * @brief This class contain functions for the database management
  14. *
  15. * This class contains functions that doesn't need to know if pdo, mysqli or whatever is used.
  16. */
  17. class DBStructure
  18. {
  19. const UPDATE_NOT_CHECKED = 0; // Database check wasn't executed before
  20. const UPDATE_SUCCESSFUL = 1; // Database check was successful
  21. const UPDATE_FAILED = 2; // Database check failed
  22. const RENAME_COLUMN = 0;
  23. const RENAME_PRIMARY_KEY = 1;
  24. /**
  25. * Database structure definition loaded from config/dbstructure.config.php
  26. *
  27. * @var array
  28. */
  29. private static $definition = [];
  30. /*
  31. * Converts all tables from MyISAM to InnoDB
  32. */
  33. public static function convertToInnoDB()
  34. {
  35. $r = q("SELECT `TABLE_NAME` FROM `information_schema`.`tables` WHERE `engine` = 'MyISAM' AND `table_schema` = '%s'",
  36. DBA::escape(DBA::databaseName()));
  37. if (!DBA::isResult($r)) {
  38. echo L10n::t('There are no tables on MyISAM.') . "\n";
  39. return;
  40. }
  41. foreach ($r AS $table) {
  42. $sql = sprintf("ALTER TABLE `%s` engine=InnoDB;", DBA::escape($table['TABLE_NAME']));
  43. echo $sql . "\n";
  44. $result = DBA::e($sql);
  45. if (!DBA::isResult($result)) {
  46. self::printUpdateError($sql);
  47. }
  48. }
  49. }
  50. /**
  51. * @brief Print out database error messages
  52. *
  53. * @param string $message Message to be added to the error message
  54. *
  55. * @return string Error message
  56. */
  57. private static function printUpdateError($message)
  58. {
  59. echo L10n::t("\nError %d occurred during database update:\n%s\n",
  60. DBA::errorNo(), DBA::errorMessage());
  61. return L10n::t('Errors encountered performing database changes: ') . $message . EOL;
  62. }
  63. public static function printStructure()
  64. {
  65. $database = self::definition(false);
  66. echo "-- ------------------------------------------\n";
  67. echo "-- " . FRIENDICA_PLATFORM . " " . FRIENDICA_VERSION . " (" . FRIENDICA_CODENAME, ")\n";
  68. echo "-- DB_UPDATE_VERSION " . DB_UPDATE_VERSION . "\n";
  69. echo "-- ------------------------------------------\n\n\n";
  70. foreach ($database AS $name => $structure) {
  71. echo "--\n";
  72. echo "-- TABLE $name\n";
  73. echo "--\n";
  74. self::createTable($name, $structure, true, false);
  75. echo "\n";
  76. }
  77. }
  78. /**
  79. * Loads the database structure definition from the config/dbstructure.config.php file.
  80. * On first pass, defines DB_UPDATE_VERSION constant.
  81. *
  82. * @see config/dbstructure.config.php
  83. * @param boolean $with_addons_structure Whether to tack on addons additional tables
  84. * @return array
  85. * @throws Exception
  86. */
  87. public static function definition($with_addons_structure = true)
  88. {
  89. if (!self::$definition) {
  90. $a = \Friendica\BaseObject::getApp();
  91. $filename = $a->getBasePath() . '/config/dbstructure.config.php';
  92. if (!is_readable($filename)) {
  93. throw new Exception('Missing database structure config file config/dbstructure.config.php');
  94. }
  95. $definition = require $filename;
  96. if (!$definition) {
  97. throw new Exception('Corrupted database structure config file config/dbstructure.config.php');
  98. }
  99. self::$definition = $definition;
  100. } else {
  101. $definition = self::$definition;
  102. }
  103. if ($with_addons_structure) {
  104. Hook::callAll('dbstructure_definition', $definition);
  105. }
  106. return $definition;
  107. }
  108. private static function createTable($name, $structure, $verbose, $action)
  109. {
  110. $r = true;
  111. $engine = "";
  112. $comment = "";
  113. $sql_rows = [];
  114. $primary_keys = [];
  115. foreach ($structure["fields"] AS $fieldname => $field) {
  116. $sql_rows[] = "`" . DBA::escape($fieldname) . "` " . self::FieldCommand($field);
  117. if (!empty($field['primary'])) {
  118. $primary_keys[] = $fieldname;
  119. }
  120. }
  121. if (!empty($structure["indexes"])) {
  122. foreach ($structure["indexes"] AS $indexname => $fieldnames) {
  123. $sql_index = self::createIndex($indexname, $fieldnames, "");
  124. if (!is_null($sql_index)) {
  125. $sql_rows[] = $sql_index;
  126. }
  127. }
  128. }
  129. if (isset($structure["engine"])) {
  130. $engine = " ENGINE=" . $structure["engine"];
  131. }
  132. if (isset($structure["comment"])) {
  133. $comment = " COMMENT='" . DBA::escape($structure["comment"]) . "'";
  134. }
  135. $sql = implode(",\n\t", $sql_rows);
  136. $sql = sprintf("CREATE TABLE IF NOT EXISTS `%s` (\n\t", DBA::escape($name)) . $sql .
  137. "\n)" . $engine . " DEFAULT COLLATE utf8mb4_general_ci" . $comment;
  138. if ($verbose) {
  139. echo $sql . ";\n";
  140. }
  141. if ($action) {
  142. $r = DBA::e($sql);
  143. }
  144. return $r;
  145. }
  146. private static function FieldCommand($parameters, $create = true)
  147. {
  148. $fieldstruct = $parameters["type"];
  149. if (isset($parameters["Collation"])) {
  150. $fieldstruct .= " COLLATE " . $parameters["Collation"];
  151. }
  152. if (isset($parameters["not null"])) {
  153. $fieldstruct .= " NOT NULL";
  154. }
  155. if (isset($parameters["default"])) {
  156. if (strpos(strtolower($parameters["type"]), "int") !== false) {
  157. $fieldstruct .= " DEFAULT " . $parameters["default"];
  158. } else {
  159. $fieldstruct .= " DEFAULT '" . $parameters["default"] . "'";
  160. }
  161. }
  162. if (isset($parameters["extra"])) {
  163. $fieldstruct .= " " . $parameters["extra"];
  164. }
  165. if (isset($parameters["comment"])) {
  166. $fieldstruct .= " COMMENT '" . DBA::escape($parameters["comment"]) . "'";
  167. }
  168. /*if (($parameters["primary"] != "") && $create)
  169. $fieldstruct .= " PRIMARY KEY";*/
  170. return ($fieldstruct);
  171. }
  172. private static function createIndex($indexname, $fieldnames, $method = "ADD")
  173. {
  174. $method = strtoupper(trim($method));
  175. if ($method != "" && $method != "ADD") {
  176. throw new Exception("Invalid parameter 'method' in self::createIndex(): '$method'");
  177. }
  178. if (in_array($fieldnames[0], ["UNIQUE", "FULLTEXT"])) {
  179. $index_type = array_shift($fieldnames);
  180. $method .= " " . $index_type;
  181. }
  182. $names = "";
  183. foreach ($fieldnames AS $fieldname) {
  184. if ($names != "") {
  185. $names .= ",";
  186. }
  187. if (preg_match('|(.+)\((\d+)\)|', $fieldname, $matches)) {
  188. $names .= "`" . DBA::escape($matches[1]) . "`(" . intval($matches[2]) . ")";
  189. } else {
  190. $names .= "`" . DBA::escape($fieldname) . "`";
  191. }
  192. }
  193. if ($indexname == "PRIMARY") {
  194. return sprintf("%s PRIMARY KEY(%s)", $method, $names);
  195. }
  196. $sql = sprintf("%s INDEX `%s` (%s)", $method, DBA::escape($indexname), $names);
  197. return ($sql);
  198. }
  199. /**
  200. * Updates DB structure and returns eventual errors messages
  201. *
  202. * @param bool $verbose
  203. * @param bool $action Whether to actually apply the update
  204. * @param bool $install Is this the initial update during the installation?
  205. * @param array $tables An array of the database tables
  206. * @param array $definition An array of the definition tables
  207. * @return string Empty string if the update is successful, error messages otherwise
  208. */
  209. public static function update($verbose, $action, $install = false, array $tables = null, array $definition = null)
  210. {
  211. if ($action && !$install) {
  212. Config::set('system', 'maintenance', 1);
  213. Config::set('system', 'maintenance_reason', L10n::t('%s: Database update', DateTimeFormat::utcNow() . ' ' . date('e')));
  214. }
  215. $errors = '';
  216. Logger::log('updating structure', Logger::DEBUG);
  217. // Get the current structure
  218. $database = [];
  219. if (is_null($tables)) {
  220. $tables = q("SHOW TABLES");
  221. }
  222. if (DBA::isResult($tables)) {
  223. foreach ($tables AS $table) {
  224. $table = current($table);
  225. Logger::log(sprintf('updating structure for table %s ...', $table), Logger::DEBUG);
  226. $database[$table] = self::tableStructure($table);
  227. }
  228. }
  229. // Get the definition
  230. if (is_null($definition)) {
  231. $definition = self::definition();
  232. }
  233. // MySQL >= 5.7.4 doesn't support the IGNORE keyword in ALTER TABLE statements
  234. if ((version_compare(DBA::serverInfo(), '5.7.4') >= 0) &&
  235. !(strpos(DBA::serverInfo(), 'MariaDB') !== false)) {
  236. $ignore = '';
  237. } else {
  238. $ignore = ' IGNORE';
  239. }
  240. // Compare it
  241. foreach ($definition AS $name => $structure) {
  242. $is_new_table = false;
  243. $group_by = "";
  244. $sql3 = "";
  245. $is_unique = false;
  246. $temp_name = $name;
  247. if (!isset($database[$name])) {
  248. $r = self::createTable($name, $structure, $verbose, $action);
  249. if (!DBA::isResult($r)) {
  250. $errors .= self::printUpdateError($name);
  251. }
  252. $is_new_table = true;
  253. } else {
  254. foreach ($structure["indexes"] AS $indexname => $fieldnames) {
  255. if (isset($database[$name]["indexes"][$indexname])) {
  256. $current_index_definition = implode(",", $database[$name]["indexes"][$indexname]);
  257. } else {
  258. $current_index_definition = "__NOT_SET__";
  259. }
  260. $new_index_definition = implode(",", $fieldnames);
  261. if ($current_index_definition != $new_index_definition) {
  262. if ($fieldnames[0] == "UNIQUE") {
  263. $is_unique = true;
  264. if ($ignore == "") {
  265. $temp_name = "temp-" . $name;
  266. }
  267. }
  268. }
  269. }
  270. /*
  271. * Drop the index if it isn't present in the definition
  272. * or the definition differ from current status
  273. * and index name doesn't start with "local_"
  274. */
  275. foreach ($database[$name]["indexes"] as $indexname => $fieldnames) {
  276. $current_index_definition = implode(",", $fieldnames);
  277. if (isset($structure["indexes"][$indexname])) {
  278. $new_index_definition = implode(",", $structure["indexes"][$indexname]);
  279. } else {
  280. $new_index_definition = "__NOT_SET__";
  281. }
  282. if ($current_index_definition != $new_index_definition && substr($indexname, 0, 6) != 'local_') {
  283. $sql2 = self::dropIndex($indexname);
  284. if ($sql3 == "") {
  285. $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
  286. } else {
  287. $sql3 .= ", " . $sql2;
  288. }
  289. }
  290. }
  291. // Compare the field structure field by field
  292. foreach ($structure["fields"] AS $fieldname => $parameters) {
  293. if (!isset($database[$name]["fields"][$fieldname])) {
  294. $sql2 = self::addTableField($fieldname, $parameters);
  295. if ($sql3 == "") {
  296. $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
  297. } else {
  298. $sql3 .= ", " . $sql2;
  299. }
  300. } else {
  301. // Compare the field definition
  302. $field_definition = $database[$name]["fields"][$fieldname];
  303. // Remove the relation data that is used for the referential integrity
  304. unset($parameters['relation']);
  305. // We change the collation after the indexes had been changed.
  306. // This is done to avoid index length problems.
  307. // So here we always ensure that there is no need to change it.
  308. unset($parameters['Collation']);
  309. unset($field_definition['Collation']);
  310. // Only update the comment when it is defined
  311. if (!isset($parameters['comment'])) {
  312. $parameters['comment'] = "";
  313. }
  314. $current_field_definition = DBA::cleanQuery(implode(",", $field_definition));
  315. $new_field_definition = DBA::cleanQuery(implode(",", $parameters));
  316. if ($current_field_definition != $new_field_definition) {
  317. $sql2 = self::modifyTableField($fieldname, $parameters);
  318. if ($sql3 == "") {
  319. $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
  320. } else {
  321. $sql3 .= ", " . $sql2;
  322. }
  323. }
  324. }
  325. }
  326. }
  327. /*
  328. * Create the index if the index don't exists in database
  329. * or the definition differ from the current status.
  330. * Don't create keys if table is new
  331. */
  332. if (!$is_new_table) {
  333. foreach ($structure["indexes"] AS $indexname => $fieldnames) {
  334. if (isset($database[$name]["indexes"][$indexname])) {
  335. $current_index_definition = implode(",", $database[$name]["indexes"][$indexname]);
  336. } else {
  337. $current_index_definition = "__NOT_SET__";
  338. }
  339. $new_index_definition = implode(",", $fieldnames);
  340. if ($current_index_definition != $new_index_definition) {
  341. $sql2 = self::createIndex($indexname, $fieldnames);
  342. // Fetch the "group by" fields for unique indexes
  343. $group_by = self::groupBy($fieldnames);
  344. if ($sql2 != "") {
  345. if ($sql3 == "") {
  346. $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
  347. } else {
  348. $sql3 .= ", " . $sql2;
  349. }
  350. }
  351. }
  352. }
  353. if (isset($database[$name]["table_status"]["Comment"])) {
  354. $structurecomment = defaults($structure, "comment", "");
  355. if ($database[$name]["table_status"]["Comment"] != $structurecomment) {
  356. $sql2 = "COMMENT = '" . DBA::escape($structurecomment) . "'";
  357. if ($sql3 == "") {
  358. $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
  359. } else {
  360. $sql3 .= ", " . $sql2;
  361. }
  362. }
  363. }
  364. if (isset($database[$name]["table_status"]["Engine"]) && isset($structure['engine'])) {
  365. if ($database[$name]["table_status"]["Engine"] != $structure['engine']) {
  366. $sql2 = "ENGINE = '" . DBA::escape($structure['engine']) . "'";
  367. if ($sql3 == "") {
  368. $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
  369. } else {
  370. $sql3 .= ", " . $sql2;
  371. }
  372. }
  373. }
  374. if (isset($database[$name]["table_status"]["Collation"])) {
  375. if ($database[$name]["table_status"]["Collation"] != 'utf8mb4_general_ci') {
  376. $sql2 = "DEFAULT COLLATE utf8mb4_general_ci";
  377. if ($sql3 == "") {
  378. $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
  379. } else {
  380. $sql3 .= ", " . $sql2;
  381. }
  382. }
  383. }
  384. if ($sql3 != "") {
  385. $sql3 .= "; ";
  386. }
  387. // Now have a look at the field collations
  388. // Compare the field structure field by field
  389. foreach ($structure["fields"] AS $fieldname => $parameters) {
  390. // Compare the field definition
  391. $field_definition = defaults($database[$name]["fields"], $fieldname, ['Collation' => '']);
  392. // Define the default collation if not given
  393. if (!isset($parameters['Collation']) && !empty($field_definition['Collation'])) {
  394. $parameters['Collation'] = 'utf8mb4_general_ci';
  395. } else {
  396. $parameters['Collation'] = null;
  397. }
  398. if ($field_definition['Collation'] != $parameters['Collation']) {
  399. $sql2 = self::modifyTableField($fieldname, $parameters);
  400. if (($sql3 == "") || (substr($sql3, -2, 2) == "; ")) {
  401. $sql3 .= "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
  402. } else {
  403. $sql3 .= ", " . $sql2;
  404. }
  405. }
  406. }
  407. }
  408. if ($sql3 != "") {
  409. if (substr($sql3, -2, 2) != "; ") {
  410. $sql3 .= ";";
  411. }
  412. $field_list = '';
  413. if ($is_unique && $ignore == '') {
  414. foreach ($database[$name]["fields"] AS $fieldname => $parameters) {
  415. $field_list .= 'ANY_VALUE(`' . $fieldname . '`),';
  416. }
  417. $field_list = rtrim($field_list, ',');
  418. }
  419. if ($verbose) {
  420. // Ensure index conversion to unique removes duplicates
  421. if ($is_unique && ($temp_name != $name)) {
  422. if ($ignore != "") {
  423. echo "SET session old_alter_table=1;\n";
  424. } else {
  425. echo "DROP TABLE IF EXISTS `" . $temp_name . "`;\n";
  426. echo "CREATE TABLE `" . $temp_name . "` LIKE `" . $name . "`;\n";
  427. }
  428. }
  429. echo $sql3 . "\n";
  430. if ($is_unique && ($temp_name != $name)) {
  431. if ($ignore != "") {
  432. echo "SET session old_alter_table=0;\n";
  433. } else {
  434. echo "INSERT INTO `" . $temp_name . "` SELECT " . DBA::anyValueFallback($field_list) . " FROM `" . $name . "`" . $group_by . ";\n";
  435. echo "DROP TABLE `" . $name . "`;\n";
  436. echo "RENAME TABLE `" . $temp_name . "` TO `" . $name . "`;\n";
  437. }
  438. }
  439. }
  440. if ($action) {
  441. if (!$install) {
  442. Config::set('system', 'maintenance_reason', L10n::t('%s: updating %s table.', DateTimeFormat::utcNow() . ' ' . date('e'), $name));
  443. }
  444. // Ensure index conversion to unique removes duplicates
  445. if ($is_unique && ($temp_name != $name)) {
  446. if ($ignore != "") {
  447. DBA::e("SET session old_alter_table=1;");
  448. } else {
  449. $r = DBA::e("DROP TABLE IF EXISTS `" . $temp_name . "`;");
  450. if (!DBA::isResult($r)) {
  451. $errors .= self::printUpdateError($sql3);
  452. return $errors;
  453. }
  454. $r = DBA::e("CREATE TABLE `" . $temp_name . "` LIKE `" . $name . "`;");
  455. if (!DBA::isResult($r)) {
  456. $errors .= self::printUpdateError($sql3);
  457. return $errors;
  458. }
  459. }
  460. }
  461. $r = DBA::e($sql3);
  462. if (!DBA::isResult($r)) {
  463. $errors .= self::printUpdateError($sql3);
  464. }
  465. if ($is_unique && ($temp_name != $name)) {
  466. if ($ignore != "") {
  467. DBA::e("SET session old_alter_table=0;");
  468. } else {
  469. $r = DBA::e("INSERT INTO `" . $temp_name . "` SELECT " . $field_list . " FROM `" . $name . "`" . $group_by . ";");
  470. if (!DBA::isResult($r)) {
  471. $errors .= self::printUpdateError($sql3);
  472. return $errors;
  473. }
  474. $r = DBA::e("DROP TABLE `" . $name . "`;");
  475. if (!DBA::isResult($r)) {
  476. $errors .= self::printUpdateError($sql3);
  477. return $errors;
  478. }
  479. $r = DBA::e("RENAME TABLE `" . $temp_name . "` TO `" . $name . "`;");
  480. if (!DBA::isResult($r)) {
  481. $errors .= self::printUpdateError($sql3);
  482. return $errors;
  483. }
  484. }
  485. }
  486. }
  487. }
  488. }
  489. if ($action && !$install) {
  490. Config::set('system', 'maintenance', 0);
  491. Config::set('system', 'maintenance_reason', '');
  492. if ($errors) {
  493. Config::set('system', 'dbupdate', self::UPDATE_FAILED);
  494. } else {
  495. Config::set('system', 'dbupdate', self::UPDATE_SUCCESSFUL);
  496. }
  497. }
  498. return $errors;
  499. }
  500. private static function tableStructure($table)
  501. {
  502. $structures = q("DESCRIBE `%s`", $table);
  503. $full_columns = q("SHOW FULL COLUMNS FROM `%s`", $table);
  504. $indexes = q("SHOW INDEX FROM `%s`", $table);
  505. $table_status = q("SHOW TABLE STATUS WHERE `name` = '%s'", $table);
  506. if (DBA::isResult($table_status)) {
  507. $table_status = $table_status[0];
  508. } else {
  509. $table_status = [];
  510. }
  511. $fielddata = [];
  512. $indexdata = [];
  513. if (DBA::isResult($indexes)) {
  514. foreach ($indexes AS $index) {
  515. if ($index["Key_name"] != "PRIMARY" && $index["Non_unique"] == "0" && !isset($indexdata[$index["Key_name"]])) {
  516. $indexdata[$index["Key_name"]] = ["UNIQUE"];
  517. }
  518. if ($index["Index_type"] == "FULLTEXT" && !isset($indexdata[$index["Key_name"]])) {
  519. $indexdata[$index["Key_name"]] = ["FULLTEXT"];
  520. }
  521. $column = $index["Column_name"];
  522. if ($index["Sub_part"] != "") {
  523. $column .= "(" . $index["Sub_part"] . ")";
  524. }
  525. $indexdata[$index["Key_name"]][] = $column;
  526. }
  527. }
  528. if (DBA::isResult($structures)) {
  529. foreach ($structures AS $field) {
  530. // Replace the default size values so that we don't have to define them
  531. $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)'];
  532. $replace = ['boolean', 'tinyint unsigned', 'tinyint', 'smallint unsigned', 'smallint', 'mediumint unsigned', 'mediumint', 'bigint', 'int unsigned', 'int'];
  533. $field["Type"] = str_replace($search, $replace, $field["Type"]);
  534. $fielddata[$field["Field"]]["type"] = $field["Type"];
  535. if ($field["Null"] == "NO") {
  536. $fielddata[$field["Field"]]["not null"] = true;
  537. }
  538. if (isset($field["Default"])) {
  539. $fielddata[$field["Field"]]["default"] = $field["Default"];
  540. }
  541. if ($field["Extra"] != "") {
  542. $fielddata[$field["Field"]]["extra"] = $field["Extra"];
  543. }
  544. if ($field["Key"] == "PRI") {
  545. $fielddata[$field["Field"]]["primary"] = true;
  546. }
  547. }
  548. }
  549. if (DBA::isResult($full_columns)) {
  550. foreach ($full_columns AS $column) {
  551. $fielddata[$column["Field"]]["Collation"] = $column["Collation"];
  552. $fielddata[$column["Field"]]["comment"] = $column["Comment"];
  553. }
  554. }
  555. return ["fields" => $fielddata, "indexes" => $indexdata, "table_status" => $table_status];
  556. }
  557. private static function dropIndex($indexname)
  558. {
  559. $sql = sprintf("DROP INDEX `%s`", DBA::escape($indexname));
  560. return ($sql);
  561. }
  562. private static function addTableField($fieldname, $parameters)
  563. {
  564. $sql = sprintf("ADD `%s` %s", DBA::escape($fieldname), self::FieldCommand($parameters));
  565. return ($sql);
  566. }
  567. private static function modifyTableField($fieldname, $parameters)
  568. {
  569. $sql = sprintf("MODIFY `%s` %s", DBA::escape($fieldname), self::FieldCommand($parameters, false));
  570. return ($sql);
  571. }
  572. /**
  573. * Constructs a GROUP BY clause from a UNIQUE index definition.
  574. *
  575. * @param array $fieldnames
  576. * @return string
  577. */
  578. private static function groupBy(array $fieldnames)
  579. {
  580. if ($fieldnames[0] != "UNIQUE") {
  581. return "";
  582. }
  583. array_shift($fieldnames);
  584. $names = "";
  585. foreach ($fieldnames AS $fieldname) {
  586. if ($names != "") {
  587. $names .= ",";
  588. }
  589. if (preg_match('|(.+)\((\d+)\)|', $fieldname, $matches)) {
  590. $names .= "`" . DBA::escape($matches[1]) . "`";
  591. } else {
  592. $names .= "`" . DBA::escape($fieldname) . "`";
  593. }
  594. }
  595. $sql = sprintf(" GROUP BY %s", $names);
  596. return $sql;
  597. }
  598. /**
  599. * Renames columns or the primary key of a table
  600. *
  601. * @todo You cannot rename a primary key if "auto increment" is set
  602. *
  603. * @param string $table Table name
  604. * @param array $columns Columns Syntax for Rename: [ $old1 => [ $new1, $type1 ], $old2 => [ $new2, $type2 ], ... ] )
  605. * Syntax for Primary Key: [ $col1, $col2, ...] )
  606. * @param int $type The type of renaming (Default is Column)
  607. *
  608. * @return boolean Was the renaming successful?
  609. *
  610. */
  611. public static function rename($table, $columns, $type = self::RENAME_COLUMN)
  612. {
  613. if (empty($table) || empty($columns)) {
  614. return false;
  615. }
  616. if (!is_array($columns)) {
  617. return false;
  618. }
  619. $table = DBA::escape($table);
  620. $sql = "ALTER TABLE `" . $table . "`";
  621. switch ($type) {
  622. case self::RENAME_COLUMN:
  623. if (!self::existsColumn($table, array_keys($columns))) {
  624. return false;
  625. }
  626. $sql .= implode(',', array_map(
  627. function ($to, $from) {
  628. return " CHANGE `" . $from . "` `" . $to[0] . "` " . $to[1];
  629. },
  630. $columns,
  631. array_keys($columns)
  632. ));
  633. break;
  634. case self::RENAME_PRIMARY_KEY:
  635. if (!self::existsColumn($table, $columns)) {
  636. return false;
  637. }
  638. $sql .= " DROP PRIMARY KEY, ADD PRIMARY KEY(`" . implode('`, `', $columns) . "`)";
  639. break;
  640. default:
  641. return false;
  642. }
  643. $sql .= ";";
  644. $stmt = DBA::p($sql);
  645. if (is_bool($stmt)) {
  646. $retval = $stmt;
  647. } else {
  648. $retval = true;
  649. }
  650. DBA::close($stmt);
  651. return $retval;
  652. }
  653. /**
  654. * Check if the columns of the table exists
  655. *
  656. * @param string $table Table name
  657. * @param array $columns Columns to check ( Syntax: [ $col1, $col2, .. ] )
  658. *
  659. * @return boolean Does the table exist?
  660. */
  661. public static function existsColumn($table, $columns = [])
  662. {
  663. if (empty($table)) {
  664. return false;
  665. }
  666. if (is_null($columns) || empty($columns)) {
  667. return self::existsTable($table);
  668. }
  669. $table = DBA::escape($table);
  670. foreach ($columns AS $column) {
  671. $sql = "SHOW COLUMNS FROM `" . $table . "` LIKE '" . $column . "';";
  672. $stmt = DBA::p($sql);
  673. if (is_bool($stmt)) {
  674. $retval = $stmt;
  675. } else {
  676. $retval = (DBA::numRows($stmt) > 0);
  677. }
  678. DBA::close($stmt);
  679. if (!$retval) {
  680. return false;
  681. }
  682. }
  683. return true;
  684. }
  685. /**
  686. * Check if a table exists
  687. *
  688. * @param string $table Table name
  689. *
  690. * @return boolean Does the table exist?
  691. */
  692. public static function existsTable($table)
  693. {
  694. if (empty($table)) {
  695. return false;
  696. }
  697. $table = DBA::escape($table);
  698. $sql = "SHOW TABLES LIKE '" . $table . "';";
  699. $stmt = DBA::p($sql);
  700. if (is_bool($stmt)) {
  701. $retval = $stmt;
  702. } else {
  703. $retval = (DBA::numRows($stmt) > 0);
  704. }
  705. DBA::close($stmt);
  706. return $retval;
  707. }
  708. }