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.

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