schema.php 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094
  1. <?php
  2. // This file is part of GNU social - https://www.gnu.org/software/social
  3. //
  4. // GNU social is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // GNU social is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Database schema
  18. *
  19. * @category Database
  20. * @package GNUsocial
  21. * @author Evan Prodromou <evan@status.net>
  22. * @author Brion Vibber <brion@status.net>
  23. * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
  24. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  25. */
  26. defined('GNUSOCIAL') || die();
  27. /**
  28. * Class representing the database schema
  29. *
  30. * A class representing the database schema. Can be used to
  31. * manipulate the schema -- especially for plugins and upgrade
  32. * utilities.
  33. *
  34. * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
  35. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  36. */
  37. class Schema
  38. {
  39. static $_static = null;
  40. protected $conn = null;
  41. /**
  42. * Constructor. Only run once for singleton object.
  43. * @param null $conn
  44. */
  45. protected function __construct($conn = null)
  46. {
  47. if (is_null($conn)) {
  48. // XXX: there should be an easier way to do this.
  49. $user = new User();
  50. $conn = $user->getDatabaseConnection();
  51. $user->free();
  52. unset($user);
  53. }
  54. $this->conn = $conn;
  55. }
  56. /**
  57. * Main public entry point. Use this to get
  58. * the schema object.
  59. *
  60. * @param null $conn
  61. * @return Schema the Schema object for the connection
  62. */
  63. static function get($conn = null)
  64. {
  65. if (is_null($conn)) {
  66. $key = 'default';
  67. } else {
  68. $key = md5(serialize($conn->dsn));
  69. }
  70. $type = common_config('db', 'type');
  71. if (empty(self::$_static[$key])) {
  72. $schemaClass = ucfirst($type) . 'Schema';
  73. self::$_static[$key] = new $schemaClass($conn);
  74. }
  75. return self::$_static[$key];
  76. }
  77. /**
  78. * Gets a ColumnDef object for a single column.
  79. *
  80. * Throws an exception if the table is not found.
  81. *
  82. * @param string $table name of the table
  83. * @param string $column name of the column
  84. *
  85. * @return ColumnDef definition of the column or null
  86. * if not found.
  87. */
  88. public function getColumnDef($table, $column)
  89. {
  90. $td = $this->getTableDef($table);
  91. if (!empty($td) && !empty($td->columns)) {
  92. foreach ($td->columns as $cd) {
  93. if ($cd->name == $column) {
  94. return $cd;
  95. }
  96. }
  97. }
  98. return null;
  99. }
  100. /**
  101. * Creates a table with the given names and columns.
  102. *
  103. * @param string $tableName Name of the table
  104. * @param array $def Table definition array listing fields and indexes.
  105. *
  106. * @return bool success flag
  107. * @throws PEAR_Exception
  108. */
  109. public function createTable($tableName, $def)
  110. {
  111. $statements = $this->buildCreateTable($tableName, $def);
  112. return $this->runSqlSet($statements);
  113. }
  114. /**
  115. * Build a set of SQL statements to create a table with the given
  116. * name and columns.
  117. *
  118. * @param string $name Name of the table
  119. * @param array $def Table definition array
  120. *
  121. * @return array success flag
  122. * @throws Exception
  123. */
  124. public function buildCreateTable($name, $def)
  125. {
  126. $def = $this->validateDef($name, $def);
  127. $def = $this->filterDef($def);
  128. $sql = [];
  129. foreach ($def['fields'] as $col => $colDef) {
  130. $this->appendColumnDef($sql, $col, $colDef);
  131. }
  132. // Primary, unique, and foreign keys are constraints, so go within
  133. // the CREATE TABLE statement normally.
  134. if (!empty($def['primary key'])) {
  135. $this->appendPrimaryKeyDef($sql, $def['primary key']);
  136. }
  137. if (!empty($def['unique keys'])) {
  138. foreach ($def['unique keys'] as $col => $colDef) {
  139. $this->appendUniqueKeyDef($sql, $col, $colDef);
  140. }
  141. }
  142. if (!empty($def['foreign keys'])) {
  143. foreach ($def['foreign keys'] as $keyName => $keyDef) {
  144. $this->appendForeignKeyDef($sql, $keyName, $keyDef);
  145. }
  146. }
  147. // Wrap the CREATE TABLE around the main body chunks...
  148. $statements = [];
  149. $statements[] = $this->startCreateTable($name, $def) . "\n" .
  150. implode($sql, ",\n") . "\n" .
  151. $this->endCreateTable($name, $def);
  152. // Multi-value indexes are advisory and for best portability
  153. // should be created as separate statements.
  154. if (!empty($def['indexes'])) {
  155. foreach ($def['indexes'] as $col => $colDef) {
  156. $this->appendCreateIndex($statements, $name, $col, $colDef);
  157. }
  158. }
  159. if (!empty($def['fulltext indexes'])) {
  160. foreach ($def['fulltext indexes'] as $col => $colDef) {
  161. $this->appendCreateFulltextIndex($statements, $name, $col, $colDef);
  162. }
  163. }
  164. return $statements;
  165. }
  166. /**
  167. * Set up a 'create table' SQL statement.
  168. *
  169. * @param string $name table name
  170. * @param array $def table definition
  171. * @return string
  172. */
  173. function startCreateTable($name, array $def)
  174. {
  175. return 'CREATE TABLE ' . $this->quoteIdentifier($name) . ' (';
  176. }
  177. /**
  178. * Close out a 'create table' SQL statement.
  179. *
  180. * @param string $name table name
  181. * @param array $def table definition
  182. * @return string
  183. */
  184. function endCreateTable($name, array $def)
  185. {
  186. return ')';
  187. }
  188. /**
  189. * Append an SQL fragment with a column definition in a CREATE TABLE statement.
  190. *
  191. * @param array $sql
  192. * @param string $name
  193. * @param array $def
  194. */
  195. function appendColumnDef(array &$sql, $name, array $def)
  196. {
  197. $sql[] = "$name " . $this->columnSql($def);
  198. }
  199. /**
  200. * Append an SQL fragment with a constraint definition for a primary
  201. * key in a CREATE TABLE statement.
  202. *
  203. * @param array $sql
  204. * @param array $def
  205. */
  206. function appendPrimaryKeyDef(array &$sql, array $def)
  207. {
  208. $sql[] = "PRIMARY KEY " . $this->buildIndexList($def);
  209. }
  210. /**
  211. * Append an SQL fragment with a constraint definition for a unique
  212. * key in a CREATE TABLE statement.
  213. *
  214. * @param array $sql
  215. * @param string $name
  216. * @param array $def
  217. */
  218. function appendUniqueKeyDef(array &$sql, $name, array $def)
  219. {
  220. $sql[] = "CONSTRAINT $name UNIQUE " . $this->buildIndexList($def);
  221. }
  222. /**
  223. * Append an SQL fragment with a constraint definition for a foreign
  224. * key in a CREATE TABLE statement.
  225. *
  226. * @param array $sql
  227. * @param string $name
  228. * @param array $def
  229. * @throws Exception
  230. */
  231. function appendForeignKeyDef(array &$sql, $name, array $def)
  232. {
  233. if (count($def) != 2) {
  234. throw new Exception("Invalid foreign key def for $name: " . var_export($def, true));
  235. }
  236. list($refTable, $map) = $def;
  237. $srcCols = array_keys($map);
  238. $refCols = array_values($map);
  239. $sql[] = "CONSTRAINT $name FOREIGN KEY " .
  240. $this->buildIndexList($srcCols) .
  241. " REFERENCES " .
  242. $this->quoteIdentifier($refTable) .
  243. " " .
  244. $this->buildIndexList($refCols);
  245. }
  246. /**
  247. * Append an SQL statement with an index definition for an advisory
  248. * index over one or more columns on a table.
  249. *
  250. * @param array $statements
  251. * @param string $table
  252. * @param string $name
  253. * @param array $def
  254. */
  255. function appendCreateIndex(array &$statements, $table, $name, array $def)
  256. {
  257. $statements[] = "CREATE INDEX $name ON $table " . $this->buildIndexList($def);
  258. }
  259. /**
  260. * Append an SQL statement with an index definition for a full-text search
  261. * index over one or more columns on a table.
  262. *
  263. * @param array $statements
  264. * @param string $table
  265. * @param string $name
  266. * @param array $def
  267. * @throws Exception
  268. */
  269. function appendCreateFulltextIndex(array &$statements, $table, $name, array $def)
  270. {
  271. throw new Exception("Fulltext index not supported in this database");
  272. }
  273. /**
  274. * Append an SQL statement to drop an index from a table.
  275. *
  276. * @param array $statements
  277. * @param string $table
  278. * @param string $name
  279. */
  280. function appendDropIndex(array &$statements, $table, $name)
  281. {
  282. $statements[] = "DROP INDEX $name ON " . $this->quoteIdentifier($table);
  283. }
  284. function buildIndexList(array $def)
  285. {
  286. // @fixme
  287. return '(' . implode(',', array_map([$this, 'buildIndexItem'], $def)) . ')';
  288. }
  289. function buildIndexItem($def)
  290. {
  291. if (is_array($def)) {
  292. list($name, $size) = $def;
  293. return $this->quoteIdentifier($name) . '(' . intval($size) . ')';
  294. }
  295. return $this->quoteIdentifier($def);
  296. }
  297. /**
  298. * Drops a table from the schema
  299. *
  300. * Throws an exception if the table is not found.
  301. *
  302. * @param string $name Name of the table to drop
  303. *
  304. * @return bool success flag
  305. * @throws PEAR_Exception
  306. */
  307. public function dropTable($name)
  308. {
  309. global $_PEAR;
  310. $res = $this->conn->query("DROP TABLE $name");
  311. if ($_PEAR->isError($res)) {
  312. PEAR_ErrorToPEAR_Exception($res);
  313. }
  314. return true;
  315. }
  316. /**
  317. * Adds an index to a table.
  318. *
  319. * If no name is provided, a name will be made up based
  320. * on the table name and column names.
  321. *
  322. * Throws an exception on database error, esp. if the table
  323. * does not exist.
  324. *
  325. * @param string $table Name of the table
  326. * @param array $columnNames Name of columns to index
  327. * @param string $name (Optional) name of the index
  328. *
  329. * @return bool success flag
  330. * @throws PEAR_Exception
  331. */
  332. public function createIndex($table, $columnNames, $name = null)
  333. {
  334. global $_PEAR;
  335. if (!is_array($columnNames)) {
  336. $columnNames = [$columnNames];
  337. }
  338. if (empty($name)) {
  339. $name = "{$table}_" . implode("_", $columnNames) . "_idx";
  340. }
  341. $res = $this->conn->query("ALTER TABLE $table " .
  342. "ADD INDEX $name (" .
  343. implode(",", $columnNames) . ")");
  344. if ($_PEAR->isError($res)) {
  345. PEAR_ErrorToPEAR_Exception($res);
  346. }
  347. return true;
  348. }
  349. /**
  350. * Drops a named index from a table.
  351. *
  352. * @param string $table name of the table the index is on.
  353. * @param string $name name of the index
  354. *
  355. * @return bool success flag
  356. * @throws PEAR_Exception
  357. */
  358. public function dropIndex($table, $name)
  359. {
  360. global $_PEAR;
  361. $res = $this->conn->query("ALTER TABLE $table DROP INDEX $name");
  362. if ($_PEAR->isError($res)) {
  363. PEAR_ErrorToPEAR_Exception($res);
  364. }
  365. return true;
  366. }
  367. /**
  368. * Adds a column to a table
  369. *
  370. * @param string $table name of the table
  371. * @param ColumnDef $columndef Definition of the new
  372. * column.
  373. *
  374. * @return bool success flag
  375. * @throws PEAR_Exception
  376. */
  377. public function addColumn($table, $columndef)
  378. {
  379. global $_PEAR;
  380. $sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef);
  381. $res = $this->conn->query($sql);
  382. if ($_PEAR->isError($res)) {
  383. PEAR_ErrorToPEAR_Exception($res);
  384. }
  385. return true;
  386. }
  387. /**
  388. * Modifies a column in the schema.
  389. *
  390. * The name must match an existing column and table.
  391. *
  392. * @param string $table name of the table
  393. * @param ColumnDef $columndef new definition of the column.
  394. *
  395. * @return bool success flag
  396. * @throws PEAR_Exception
  397. */
  398. public function modifyColumn($table, $columndef)
  399. {
  400. global $_PEAR;
  401. $sql = "ALTER TABLE $table MODIFY COLUMN " .
  402. $this->_columnSql($columndef);
  403. $res = $this->conn->query($sql);
  404. if ($_PEAR->isError($res)) {
  405. PEAR_ErrorToPEAR_Exception($res);
  406. }
  407. return true;
  408. }
  409. /**
  410. * Drops a column from a table
  411. *
  412. * The name must match an existing column.
  413. *
  414. * @param string $table name of the table
  415. * @param string $columnName name of the column to drop
  416. *
  417. * @return bool success flag
  418. * @throws PEAR_Exception
  419. */
  420. public function dropColumn($table, $columnName)
  421. {
  422. global $_PEAR;
  423. $sql = "ALTER TABLE $table DROP COLUMN $columnName";
  424. $res = $this->conn->query($sql);
  425. if ($_PEAR->isError($res)) {
  426. PEAR_ErrorToPEAR_Exception($res);
  427. }
  428. return true;
  429. }
  430. /**
  431. * Ensures that a table exists with the given
  432. * name and the given column definitions.
  433. *
  434. * If the table does not yet exist, it will
  435. * create the table. If it does exist, it will
  436. * alter the table to match the column definitions.
  437. *
  438. * @param string $tableName name of the table
  439. * @param array $def Table definition array
  440. *
  441. * @return bool success flag
  442. * @throws PEAR_Exception
  443. */
  444. public function ensureTable($tableName, $def)
  445. {
  446. $statements = $this->buildEnsureTable($tableName, $def);
  447. return $this->runSqlSet($statements);
  448. }
  449. /**
  450. * Run a given set of SQL commands on the connection in sequence.
  451. * Empty input is ok.
  452. *
  453. * @fixme if multiple statements, wrap in a transaction?
  454. * @param array $statements
  455. * @return bool success flag
  456. * @throws PEAR_Exception
  457. */
  458. function runSqlSet(array $statements)
  459. {
  460. global $_PEAR;
  461. $ok = true;
  462. foreach ($statements as $sql) {
  463. if (defined('DEBUG_INSTALLER')) {
  464. echo "<code>" . htmlspecialchars($sql) . "</code><br/>\n";
  465. }
  466. $res = $this->conn->query($sql);
  467. if ($_PEAR->isError($res)) {
  468. common_debug('PEAR exception on query: ' . $sql);
  469. PEAR_ErrorToPEAR_Exception($res);
  470. }
  471. }
  472. return $ok;
  473. }
  474. /**
  475. * Check a table's status, and if needed build a set
  476. * of SQL statements which change it to be consistent
  477. * with the given table definition.
  478. *
  479. * If the table does not yet exist, statements will
  480. * be returned to create the table. If it does exist,
  481. * statements will be returned to alter the table to
  482. * match the column definitions.
  483. *
  484. * @param string $tableName name of the table
  485. * @param array $def
  486. * @return array of SQL statements
  487. * @throws Exception
  488. */
  489. function buildEnsureTable($tableName, array $def)
  490. {
  491. try {
  492. $old = $this->getTableDef($tableName);
  493. } catch (SchemaTableMissingException $e) {
  494. return $this->buildCreateTable($tableName, $def);
  495. }
  496. // Filter the DB-independent table definition to match the current
  497. // database engine's features and limitations.
  498. $def = $this->validateDef($tableName, $def);
  499. $def = $this->filterDef($def);
  500. $statements = [];
  501. $fields = $this->diffArrays($old, $def, 'fields', [$this, 'columnsEqual']);
  502. $uniques = $this->diffArrays($old, $def, 'unique keys');
  503. $indexes = $this->diffArrays($old, $def, 'indexes');
  504. $foreign = $this->diffArrays($old, $def, 'foreign keys');
  505. $fulltext = $this->diffArrays($old, $def, 'fulltext indexes');
  506. // Drop any obsolete or modified indexes ahead...
  507. foreach ($indexes['del'] + $indexes['mod'] as $indexName) {
  508. $this->appendDropIndex($statements, $tableName, $indexName);
  509. }
  510. // Drop any obsolete or modified fulltext indexes ahead...
  511. foreach ($fulltext['del'] + $fulltext['mod'] as $indexName) {
  512. $this->appendDropIndex($statements, $tableName, $indexName);
  513. }
  514. // For efficiency, we want this all in one
  515. // query, instead of using our methods.
  516. $phrase = [];
  517. foreach ($foreign['del'] + $foreign['mod'] as $keyName) {
  518. $this->appendAlterDropForeign($phrase, $keyName);
  519. }
  520. foreach ($uniques['del'] + $uniques['mod'] as $keyName) {
  521. $this->appendAlterDropUnique($phrase, $keyName);
  522. }
  523. if (isset($old['primary key']) && (!isset($def['primary key']) || $def['primary key'] != $old['primary key'])) {
  524. $this->appendAlterDropPrimary($phrase);
  525. }
  526. foreach ($fields['add'] as $columnName) {
  527. $this->appendAlterAddColumn($phrase, $columnName,
  528. $def['fields'][$columnName]);
  529. }
  530. foreach ($fields['mod'] as $columnName) {
  531. $this->appendAlterModifyColumn($phrase, $columnName,
  532. $old['fields'][$columnName],
  533. $def['fields'][$columnName]);
  534. }
  535. foreach ($fields['del'] as $columnName) {
  536. $this->appendAlterDropColumn($phrase, $columnName);
  537. }
  538. if (isset($def['primary key']) && (!isset($old['primary key']) || $old['primary key'] != $def['primary key'])) {
  539. $this->appendAlterAddPrimary($phrase, $def['primary key']);
  540. }
  541. foreach ($uniques['mod'] + $uniques['add'] as $keyName) {
  542. $this->appendAlterAddUnique($phrase, $keyName, $def['unique keys'][$keyName]);
  543. }
  544. foreach ($foreign['mod'] + $foreign['add'] as $keyName) {
  545. $this->appendAlterAddForeign($phrase, $keyName, $def['foreign keys'][$keyName]);
  546. }
  547. $this->appendAlterExtras($phrase, $tableName, $def);
  548. if (count($phrase) > 0) {
  549. $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(",\n", $phrase);
  550. $statements[] = $sql;
  551. }
  552. // Now create any indexes...
  553. foreach ($indexes['mod'] + $indexes['add'] as $indexName) {
  554. $this->appendCreateIndex($statements, $tableName, $indexName, $def['indexes'][$indexName]);
  555. }
  556. foreach ($fulltext['mod'] + $fulltext['add'] as $indexName) {
  557. $colDef = $def['fulltext indexes'][$indexName];
  558. $this->appendCreateFulltextIndex($statements, $tableName, $indexName, $colDef);
  559. }
  560. return $statements;
  561. }
  562. function diffArrays($oldDef, $newDef, $section, $compareCallback = null)
  563. {
  564. $old = isset($oldDef[$section]) ? $oldDef[$section] : [];
  565. $new = isset($newDef[$section]) ? $newDef[$section] : [];
  566. $oldKeys = array_keys($old);
  567. $newKeys = array_keys($new);
  568. $toadd = array_diff($newKeys, $oldKeys);
  569. $todrop = array_diff($oldKeys, $newKeys);
  570. $same = array_intersect($newKeys, $oldKeys);
  571. $tomod = [];
  572. $tokeep = [];
  573. // Find which fields have actually changed definition
  574. // in a way that we need to tweak them for this DB type.
  575. foreach ($same as $name) {
  576. if ($compareCallback) {
  577. $same = call_user_func($compareCallback, $old[$name], $new[$name]);
  578. } else {
  579. $same = ($old[$name] == $new[$name]);
  580. }
  581. if ($same) {
  582. $tokeep[] = $name;
  583. continue;
  584. }
  585. $tomod[] = $name;
  586. }
  587. return [
  588. 'add' => $toadd,
  589. 'del' => $todrop,
  590. 'mod' => $tomod,
  591. 'keep' => $tokeep,
  592. 'count' => count($toadd) + count($todrop) + count($tomod)
  593. ];
  594. }
  595. /**
  596. * Append phrase(s) to an array of partial ALTER TABLE chunks in order
  597. * to add the given column definition to the table.
  598. *
  599. * @param array $phrase
  600. * @param string $columnName
  601. * @param array $cd
  602. */
  603. function appendAlterAddColumn(array &$phrase, $columnName, array $cd)
  604. {
  605. $phrase[] = 'ADD COLUMN ' .
  606. $this->quoteIdentifier($columnName) .
  607. ' ' .
  608. $this->columnSql($cd);
  609. }
  610. /**
  611. * Append phrase(s) to an array of partial ALTER TABLE chunks in order
  612. * to alter the given column from its old state to a new one.
  613. *
  614. * @param array $phrase
  615. * @param string $columnName
  616. * @param array $old previous column definition as found in DB
  617. * @param array $cd current column definition
  618. */
  619. function appendAlterModifyColumn(array &$phrase, $columnName, array $old, array $cd)
  620. {
  621. $phrase[] = 'MODIFY COLUMN ' .
  622. $this->quoteIdentifier($columnName) .
  623. ' ' .
  624. $this->columnSql($cd);
  625. }
  626. /**
  627. * Append phrase(s) to an array of partial ALTER TABLE chunks in order
  628. * to drop the given column definition from the table.
  629. *
  630. * @param array $phrase
  631. * @param string $columnName
  632. */
  633. function appendAlterDropColumn(array &$phrase, $columnName)
  634. {
  635. $phrase[] = 'DROP COLUMN ' . $this->quoteIdentifier($columnName);
  636. }
  637. function appendAlterAddUnique(array &$phrase, $keyName, array $def)
  638. {
  639. $sql = [];
  640. $sql[] = 'ADD';
  641. $this->appendUniqueKeyDef($sql, $keyName, $def);
  642. $phrase[] = implode(' ', $sql);
  643. }
  644. function appendAlterAddForeign(array &$phrase, $keyName, array $def)
  645. {
  646. $sql = [];
  647. $sql[] = 'ADD';
  648. $this->appendForeignKeyDef($sql, $keyName, $def);
  649. $phrase[] = implode(' ', $sql);
  650. }
  651. function appendAlterAddPrimary(array &$phrase, array $def)
  652. {
  653. $sql = [];
  654. $sql[] = 'ADD';
  655. $this->appendPrimaryKeyDef($sql, $def);
  656. $phrase[] = implode(' ', $sql);
  657. }
  658. function appendAlterDropPrimary(array &$phrase)
  659. {
  660. $phrase[] = 'DROP CONSTRAINT PRIMARY KEY';
  661. }
  662. function appendAlterDropUnique(array &$phrase, $keyName)
  663. {
  664. $phrase[] = 'DROP CONSTRAINT ' . $keyName;
  665. }
  666. function appendAlterDropForeign(array &$phrase, $keyName)
  667. {
  668. $phrase[] = 'DROP FOREIGN KEY ' . $keyName;
  669. }
  670. function appendAlterExtras(array &$phrase, $tableName, array $def)
  671. {
  672. // no-op
  673. }
  674. /**
  675. * Quote a db/table/column identifier if necessary.
  676. *
  677. * @param string $name
  678. * @return string
  679. */
  680. function quoteIdentifier($name)
  681. {
  682. return $name;
  683. }
  684. function quoteDefaultValue($cd)
  685. {
  686. if (($cd['type'] == 'datetime' || $cd['type'] == 'timestamp') && $cd['default'] == 'CURRENT_TIMESTAMP') {
  687. return $cd['default'];
  688. } else {
  689. return $this->quoteValue($cd['default']);
  690. }
  691. }
  692. function quoteValue($val)
  693. {
  694. return $this->conn->quoteSmart($val);
  695. }
  696. /**
  697. * Check if two column definitions are equivalent.
  698. * The default implementation checks _everything_ but in many cases
  699. * you may be able to discard a bunch of equivalencies.
  700. *
  701. * @param array $a
  702. * @param array $b
  703. * @return bool
  704. */
  705. function columnsEqual(array $a, array $b)
  706. {
  707. return !array_diff_assoc($a, $b) && !array_diff_assoc($b, $a);
  708. }
  709. /**
  710. * Returns the array of names from an array of
  711. * ColumnDef objects.
  712. *
  713. * @param array $cds array of ColumnDef objects
  714. *
  715. * @return array strings for name values
  716. */
  717. protected function _names($cds)
  718. {
  719. $names = [];
  720. foreach ($cds as $cd) {
  721. $names[] = $cd->name;
  722. }
  723. return $names;
  724. }
  725. /**
  726. * Get a ColumnDef from an array matching
  727. * name.
  728. *
  729. * @param array $cds Array of ColumnDef objects
  730. * @param string $name Name of the column
  731. *
  732. * @return ColumnDef matching item or null if no match.
  733. */
  734. protected function _byName($cds, $name)
  735. {
  736. foreach ($cds as $cd) {
  737. if ($cd->name == $name) {
  738. return $cd;
  739. }
  740. }
  741. return null;
  742. }
  743. /**
  744. * Return the proper SQL for creating or
  745. * altering a column.
  746. *
  747. * Appropriate for use in CREATE TABLE or
  748. * ALTER TABLE statements.
  749. *
  750. * @param array $cd column to create
  751. *
  752. * @return string correct SQL for that column
  753. */
  754. function columnSql(array $cd)
  755. {
  756. $line = [];
  757. $line[] = $this->typeAndSize($cd);
  758. if (isset($cd['default'])) {
  759. $line[] = 'default';
  760. $line[] = $this->quoteDefaultValue($cd);
  761. } else if (!empty($cd['not null'])) {
  762. // Can't have both not null AND default!
  763. $line[] = 'not null';
  764. }
  765. return implode(' ', $line);
  766. }
  767. /**
  768. *
  769. * @param string $column canonical type name in defs
  770. * @return string native DB type name
  771. */
  772. function mapType($column)
  773. {
  774. return $column;
  775. }
  776. function typeAndSize($column)
  777. {
  778. //$type = $this->mapType($column);
  779. $type = $column['type'];
  780. if (isset($column['size'])) {
  781. $type = $column['size'] . $type;
  782. }
  783. $lengths = [];
  784. if (isset($column['precision'])) {
  785. $lengths[] = $column['precision'];
  786. if (isset($column['scale'])) {
  787. $lengths[] = $column['scale'];
  788. }
  789. } else if (isset($column['length'])) {
  790. $lengths[] = $column['length'];
  791. }
  792. if ($lengths) {
  793. return $type . '(' . implode(',', $lengths) . ')';
  794. } else {
  795. return $type;
  796. }
  797. }
  798. /**
  799. * Convert an old-style set of ColumnDef objects into the current
  800. * Drupal-style schema definition array, for backwards compatibility
  801. * with plugins written for 0.9.x.
  802. *
  803. * @param string $tableName
  804. * @param array $defs : array of ColumnDef objects
  805. * @return array
  806. */
  807. protected function oldToNew($tableName, array $defs)
  808. {
  809. $table = [];
  810. $prefixes = [
  811. 'tiny',
  812. 'small',
  813. 'medium',
  814. 'big',
  815. ];
  816. foreach ($defs as $cd) {
  817. $column = [];
  818. $column['type'] = $cd->type;
  819. foreach ($prefixes as $prefix) {
  820. if (substr($cd->type, 0, strlen($prefix)) == $prefix) {
  821. $column['type'] = substr($cd->type, strlen($prefix));
  822. $column['size'] = $prefix;
  823. break;
  824. }
  825. }
  826. if ($cd->size) {
  827. if ($cd->type == 'varchar' || $cd->type == 'char') {
  828. $column['length'] = $cd->size;
  829. }
  830. }
  831. if (!$cd->nullable) {
  832. $column['not null'] = true;
  833. }
  834. if ($cd->auto_increment) {
  835. $column['type'] = 'serial';
  836. }
  837. if ($cd->default) {
  838. $column['default'] = $cd->default;
  839. }
  840. $table['fields'][$cd->name] = $column;
  841. if ($cd->key == 'PRI') {
  842. // If multiple columns are defined as primary key,
  843. // we'll pile them on in sequence.
  844. if (!isset($table['primary key'])) {
  845. $table['primary key'] = [];
  846. }
  847. $table['primary key'][] = $cd->name;
  848. } else if ($cd->key == 'MUL') {
  849. // Individual multiple-value indexes are only per-column
  850. // using the old ColumnDef syntax.
  851. $idx = "{$tableName}_{$cd->name}_idx";
  852. $table['indexes'][$idx] = [$cd->name];
  853. } else if ($cd->key == 'UNI') {
  854. // Individual unique-value indexes are only per-column
  855. // using the old ColumnDef syntax.
  856. $idx = "{$tableName}_{$cd->name}_idx";
  857. $table['unique keys'][$idx] = [$cd->name];
  858. }
  859. }
  860. return $table;
  861. }
  862. /**
  863. * Filter the given table definition array to match features available
  864. * in this database.
  865. *
  866. * This lets us strip out unsupported things like comments, foreign keys,
  867. * or type variants that we wouldn't get back from getTableDef().
  868. *
  869. * @param array $tableDef
  870. * @return array
  871. */
  872. function filterDef(array $tableDef)
  873. {
  874. return $tableDef;
  875. }
  876. /**
  877. * Validate a table definition array, checking for basic structure.
  878. *
  879. * If necessary, converts from an old-style array of ColumnDef objects.
  880. *
  881. * @param string $tableName
  882. * @param array $def : table definition array
  883. * @return array validated table definition array
  884. *
  885. * @throws Exception on wildly invalid input
  886. */
  887. function validateDef($tableName, array $def)
  888. {
  889. if (isset($def[0]) && $def[0] instanceof ColumnDef) {
  890. $def = $this->oldToNew($tableName, $def);
  891. }
  892. // A few quick checks :D
  893. if (!isset($def['fields'])) {
  894. throw new Exception("Invalid table definition for $tableName: no fields.");
  895. }
  896. return $def;
  897. }
  898. function isNumericType($type)
  899. {
  900. $type = strtolower($type);
  901. $known = ['int', 'serial', 'numeric'];
  902. return in_array($type, $known);
  903. }
  904. /**
  905. * Pull info from the query into a fun-fun array of dooooom
  906. *
  907. * @param string $sql
  908. * @return array of arrays
  909. * @throws PEAR_Exception
  910. */
  911. protected function fetchQueryData($sql)
  912. {
  913. global $_PEAR;
  914. $res = $this->conn->query($sql);
  915. if ($_PEAR->isError($res)) {
  916. PEAR_ErrorToPEAR_Exception($res);
  917. }
  918. $out = [];
  919. $row = [];
  920. while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
  921. $out[] = $row;
  922. }
  923. $res->free();
  924. return $out;
  925. }
  926. public function renameTable(string $old_name, string $new_name) : bool
  927. {
  928. try {
  929. $this->getTableDef($old_name);
  930. try {
  931. $this->getTableDef($new_name);
  932. // New table exists, can't work
  933. throw new ServerException("Both table {$old_name} and {$new_name} exist. You're on your own.");
  934. } catch(SchemaTableMissingException $e) {
  935. // New table doesn't exist, carry on
  936. }
  937. } catch(SchemaTableMissingException $e) {
  938. // Already renamed, or no previous table, so we're done
  939. return true;
  940. }
  941. return $this->runSqlSet(["ALTER TABLE {$old_name} RENAME TO {$new_name};"]);
  942. }
  943. }
  944. class SchemaTableMissingException extends Exception
  945. {
  946. // no-op
  947. }