schema.php 30 KB

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