Schema.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. <?php
  2. namespace Drupal\Core\Database\Driver\mysql;
  3. use Drupal\Core\Database\SchemaException;
  4. use Drupal\Core\Database\SchemaObjectExistsException;
  5. use Drupal\Core\Database\SchemaObjectDoesNotExistException;
  6. use Drupal\Core\Database\Schema as DatabaseSchema;
  7. use Drupal\Component\Utility\Unicode;
  8. /**
  9. * @addtogroup schemaapi
  10. * @{
  11. */
  12. /**
  13. * MySQL implementation of \Drupal\Core\Database\Schema.
  14. */
  15. class Schema extends DatabaseSchema {
  16. /**
  17. * Maximum length of a table comment in MySQL.
  18. */
  19. const COMMENT_MAX_TABLE = 60;
  20. /**
  21. * Maximum length of a column comment in MySQL.
  22. */
  23. const COMMENT_MAX_COLUMN = 255;
  24. /**
  25. * @var array
  26. * List of MySQL string types.
  27. */
  28. protected $mysqlStringTypes = [
  29. 'VARCHAR',
  30. 'CHAR',
  31. 'TINYTEXT',
  32. 'MEDIUMTEXT',
  33. 'LONGTEXT',
  34. 'TEXT',
  35. ];
  36. /**
  37. * Get information about the table and database name from the prefix.
  38. *
  39. * @return
  40. * A keyed array with information about the database, table name and prefix.
  41. */
  42. protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
  43. $info = ['prefix' => $this->connection->tablePrefix($table)];
  44. if ($add_prefix) {
  45. $table = $info['prefix'] . $table;
  46. }
  47. if (($pos = strpos($table, '.')) !== FALSE) {
  48. $info['database'] = substr($table, 0, $pos);
  49. $info['table'] = substr($table, ++$pos);
  50. }
  51. else {
  52. $info['database'] = $this->connection->getConnectionOptions()['database'];
  53. $info['table'] = $table;
  54. }
  55. return $info;
  56. }
  57. /**
  58. * Build a condition to match a table name against a standard information_schema.
  59. *
  60. * MySQL uses databases like schemas rather than catalogs so when we build
  61. * a condition to query the information_schema.tables, we set the default
  62. * database as the schema unless specified otherwise, and exclude table_catalog
  63. * from the condition criteria.
  64. */
  65. protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
  66. $table_info = $this->getPrefixInfo($table_name, $add_prefix);
  67. $condition = $this->connection->condition('AND');
  68. $condition->condition('table_schema', $table_info['database']);
  69. $condition->condition('table_name', $table_info['table'], $operator);
  70. return $condition;
  71. }
  72. /**
  73. * Generate SQL to create a new table from a Drupal schema definition.
  74. *
  75. * @param $name
  76. * The name of the table to create.
  77. * @param $table
  78. * A Schema API table definition array.
  79. *
  80. * @return
  81. * An array of SQL statements to create the table.
  82. */
  83. protected function createTableSql($name, $table) {
  84. $info = $this->connection->getConnectionOptions();
  85. // Provide defaults if needed.
  86. $table += [
  87. 'mysql_engine' => 'InnoDB',
  88. 'mysql_character_set' => 'utf8mb4',
  89. ];
  90. $sql = "CREATE TABLE {" . $name . "} (\n";
  91. // Add the SQL statement for each field.
  92. foreach ($table['fields'] as $field_name => $field) {
  93. $sql .= $this->createFieldSql($field_name, $this->processField($field)) . ", \n";
  94. }
  95. // Process keys & indexes.
  96. if (!empty($table['primary key']) && is_array($table['primary key'])) {
  97. $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']);
  98. }
  99. $keys = $this->createKeysSql($table);
  100. if (count($keys)) {
  101. $sql .= implode(", \n", $keys) . ", \n";
  102. }
  103. // Remove the last comma and space.
  104. $sql = substr($sql, 0, -3) . "\n) ";
  105. $sql .= 'ENGINE = ' . $table['mysql_engine'] . ' DEFAULT CHARACTER SET ' . $table['mysql_character_set'];
  106. // By default, MySQL uses the default collation for new tables, which is
  107. // 'utf8mb4_general_ci' (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) for
  108. // utf8mb4. If an alternate collation has been set, it needs to be
  109. // explicitly specified.
  110. // @see \Drupal\Core\Database\Driver\mysql\Schema
  111. if (!empty($info['collation'])) {
  112. $sql .= ' COLLATE ' . $info['collation'];
  113. }
  114. // Add table comment.
  115. if (!empty($table['description'])) {
  116. $sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE);
  117. }
  118. return [$sql];
  119. }
  120. /**
  121. * Create an SQL string for a field to be used in table creation or alteration.
  122. *
  123. * Before passing a field out of a schema definition into this function it has
  124. * to be processed by _db_process_field().
  125. *
  126. * @param string $name
  127. * Name of the field.
  128. * @param array $spec
  129. * The field specification, as per the schema data structure format.
  130. */
  131. protected function createFieldSql($name, $spec) {
  132. $sql = "`" . $name . "` " . $spec['mysql_type'];
  133. if (in_array($spec['mysql_type'], $this->mysqlStringTypes)) {
  134. if (isset($spec['length'])) {
  135. $sql .= '(' . $spec['length'] . ')';
  136. }
  137. if (isset($spec['type']) && $spec['type'] == 'varchar_ascii') {
  138. $sql .= ' CHARACTER SET ascii';
  139. }
  140. if (!empty($spec['binary'])) {
  141. $sql .= ' BINARY';
  142. }
  143. // Note we check for the "type" key here. "mysql_type" is VARCHAR:
  144. elseif (isset($spec['type']) && $spec['type'] == 'varchar_ascii') {
  145. $sql .= ' COLLATE ascii_general_ci';
  146. }
  147. }
  148. elseif (isset($spec['precision']) && isset($spec['scale'])) {
  149. $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
  150. }
  151. if (!empty($spec['unsigned'])) {
  152. $sql .= ' unsigned';
  153. }
  154. if (isset($spec['not null'])) {
  155. if ($spec['not null']) {
  156. $sql .= ' NOT NULL';
  157. }
  158. else {
  159. $sql .= ' NULL';
  160. }
  161. }
  162. if (!empty($spec['auto_increment'])) {
  163. $sql .= ' auto_increment';
  164. }
  165. // $spec['default'] can be NULL, so we explicitly check for the key here.
  166. if (array_key_exists('default', $spec)) {
  167. $sql .= ' DEFAULT ' . $this->escapeDefaultValue($spec['default']);
  168. }
  169. if (empty($spec['not null']) && !isset($spec['default'])) {
  170. $sql .= ' DEFAULT NULL';
  171. }
  172. // Add column comment.
  173. if (!empty($spec['description'])) {
  174. $sql .= ' COMMENT ' . $this->prepareComment($spec['description'], self::COMMENT_MAX_COLUMN);
  175. }
  176. return $sql;
  177. }
  178. /**
  179. * Set database-engine specific properties for a field.
  180. *
  181. * @param $field
  182. * A field description array, as specified in the schema documentation.
  183. */
  184. protected function processField($field) {
  185. if (!isset($field['size'])) {
  186. $field['size'] = 'normal';
  187. }
  188. // Set the correct database-engine specific datatype.
  189. // In case one is already provided, force it to uppercase.
  190. if (isset($field['mysql_type'])) {
  191. $field['mysql_type'] = mb_strtoupper($field['mysql_type']);
  192. }
  193. else {
  194. $map = $this->getFieldTypeMap();
  195. $field['mysql_type'] = $map[$field['type'] . ':' . $field['size']];
  196. }
  197. if (isset($field['type']) && $field['type'] == 'serial') {
  198. $field['auto_increment'] = TRUE;
  199. }
  200. return $field;
  201. }
  202. /**
  203. * {@inheritdoc}
  204. */
  205. public function getFieldTypeMap() {
  206. // Put :normal last so it gets preserved by array_flip. This makes
  207. // it much easier for modules (such as schema.module) to map
  208. // database types back into schema types.
  209. // $map does not use drupal_static as its value never changes.
  210. static $map = [
  211. 'varchar_ascii:normal' => 'VARCHAR',
  212. 'varchar:normal' => 'VARCHAR',
  213. 'char:normal' => 'CHAR',
  214. 'text:tiny' => 'TINYTEXT',
  215. 'text:small' => 'TINYTEXT',
  216. 'text:medium' => 'MEDIUMTEXT',
  217. 'text:big' => 'LONGTEXT',
  218. 'text:normal' => 'TEXT',
  219. 'serial:tiny' => 'TINYINT',
  220. 'serial:small' => 'SMALLINT',
  221. 'serial:medium' => 'MEDIUMINT',
  222. 'serial:big' => 'BIGINT',
  223. 'serial:normal' => 'INT',
  224. 'int:tiny' => 'TINYINT',
  225. 'int:small' => 'SMALLINT',
  226. 'int:medium' => 'MEDIUMINT',
  227. 'int:big' => 'BIGINT',
  228. 'int:normal' => 'INT',
  229. 'float:tiny' => 'FLOAT',
  230. 'float:small' => 'FLOAT',
  231. 'float:medium' => 'FLOAT',
  232. 'float:big' => 'DOUBLE',
  233. 'float:normal' => 'FLOAT',
  234. 'numeric:normal' => 'DECIMAL',
  235. 'blob:big' => 'LONGBLOB',
  236. 'blob:normal' => 'BLOB',
  237. ];
  238. return $map;
  239. }
  240. protected function createKeysSql($spec) {
  241. $keys = [];
  242. if (!empty($spec['primary key'])) {
  243. $keys[] = 'PRIMARY KEY (' . $this->createKeySql($spec['primary key']) . ')';
  244. }
  245. if (!empty($spec['unique keys'])) {
  246. foreach ($spec['unique keys'] as $key => $fields) {
  247. $keys[] = 'UNIQUE KEY `' . $key . '` (' . $this->createKeySql($fields) . ')';
  248. }
  249. }
  250. if (!empty($spec['indexes'])) {
  251. $indexes = $this->getNormalizedIndexes($spec);
  252. foreach ($indexes as $index => $fields) {
  253. $keys[] = 'INDEX `' . $index . '` (' . $this->createKeySql($fields) . ')';
  254. }
  255. }
  256. return $keys;
  257. }
  258. /**
  259. * Gets normalized indexes from a table specification.
  260. *
  261. * Shortens indexes to 191 characters if they apply to utf8mb4-encoded
  262. * fields, in order to comply with the InnoDB index limitation of 756 bytes.
  263. *
  264. * @param array $spec
  265. * The table specification.
  266. *
  267. * @return array
  268. * List of shortened indexes.
  269. *
  270. * @throws \Drupal\Core\Database\SchemaException
  271. * Thrown if field specification is missing.
  272. */
  273. protected function getNormalizedIndexes(array $spec) {
  274. $indexes = isset($spec['indexes']) ? $spec['indexes'] : [];
  275. foreach ($indexes as $index_name => $index_fields) {
  276. foreach ($index_fields as $index_key => $index_field) {
  277. // Get the name of the field from the index specification.
  278. $field_name = is_array($index_field) ? $index_field[0] : $index_field;
  279. // Check whether the field is defined in the table specification.
  280. if (isset($spec['fields'][$field_name])) {
  281. // Get the MySQL type from the processed field.
  282. $mysql_field = $this->processField($spec['fields'][$field_name]);
  283. if (in_array($mysql_field['mysql_type'], $this->mysqlStringTypes)) {
  284. // Check whether we need to shorten the index.
  285. if ((!isset($mysql_field['type']) || $mysql_field['type'] != 'varchar_ascii') && (!isset($mysql_field['length']) || $mysql_field['length'] > 191)) {
  286. // Limit the index length to 191 characters.
  287. $this->shortenIndex($indexes[$index_name][$index_key]);
  288. }
  289. }
  290. }
  291. else {
  292. throw new SchemaException("MySQL needs the '$field_name' field specification in order to normalize the '$index_name' index");
  293. }
  294. }
  295. }
  296. return $indexes;
  297. }
  298. /**
  299. * Helper function for normalizeIndexes().
  300. *
  301. * Shortens an index to 191 characters.
  302. *
  303. * @param array $index
  304. * The index array to be used in createKeySql.
  305. *
  306. * @see Drupal\Core\Database\Driver\mysql\Schema::createKeySql()
  307. * @see Drupal\Core\Database\Driver\mysql\Schema::normalizeIndexes()
  308. */
  309. protected function shortenIndex(&$index) {
  310. if (is_array($index)) {
  311. if ($index[1] > 191) {
  312. $index[1] = 191;
  313. }
  314. }
  315. else {
  316. $index = [$index, 191];
  317. }
  318. }
  319. protected function createKeySql($fields) {
  320. $return = [];
  321. foreach ($fields as $field) {
  322. if (is_array($field)) {
  323. $return[] = '`' . $field[0] . '`(' . $field[1] . ')';
  324. }
  325. else {
  326. $return[] = '`' . $field . '`';
  327. }
  328. }
  329. return implode(', ', $return);
  330. }
  331. /**
  332. * {@inheritdoc}
  333. */
  334. public function renameTable($table, $new_name) {
  335. if (!$this->tableExists($table)) {
  336. throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist.");
  337. }
  338. if ($this->tableExists($new_name)) {
  339. throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists.");
  340. }
  341. $info = $this->getPrefixInfo($new_name);
  342. $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO `' . $info['table'] . '`');
  343. }
  344. /**
  345. * {@inheritdoc}
  346. */
  347. public function dropTable($table) {
  348. if (!$this->tableExists($table)) {
  349. return FALSE;
  350. }
  351. $this->connection->query('DROP TABLE {' . $table . '}');
  352. return TRUE;
  353. }
  354. /**
  355. * {@inheritdoc}
  356. */
  357. public function addField($table, $field, $spec, $keys_new = []) {
  358. if (!$this->tableExists($table)) {
  359. throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist.");
  360. }
  361. if ($this->fieldExists($table, $field)) {
  362. throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists.");
  363. }
  364. // Fields that are part of a PRIMARY KEY must be added as NOT NULL.
  365. $is_primary_key = isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE);
  366. if ($is_primary_key) {
  367. $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field => $spec]);
  368. }
  369. $fixnull = FALSE;
  370. if (!empty($spec['not null']) && !isset($spec['default']) && !$is_primary_key) {
  371. $fixnull = TRUE;
  372. $spec['not null'] = FALSE;
  373. }
  374. $query = 'ALTER TABLE {' . $table . '} ADD ';
  375. $query .= $this->createFieldSql($field, $this->processField($spec));
  376. if ($keys_sql = $this->createKeysSql($keys_new)) {
  377. // Make sure to drop the existing primary key before adding a new one.
  378. // This is only needed when adding a field because this method, unlike
  379. // changeField(), is supposed to handle primary keys automatically.
  380. if (isset($keys_new['primary key']) && $this->indexExists($table, 'PRIMARY')) {
  381. $query .= ', DROP PRIMARY KEY';
  382. }
  383. $query .= ', ADD ' . implode(', ADD ', $keys_sql);
  384. }
  385. $this->connection->query($query);
  386. if (isset($spec['initial_from_field'])) {
  387. if (isset($spec['initial'])) {
  388. $expression = 'COALESCE(' . $spec['initial_from_field'] . ', :default_initial_value)';
  389. $arguments = [':default_initial_value' => $spec['initial']];
  390. }
  391. else {
  392. $expression = $spec['initial_from_field'];
  393. $arguments = [];
  394. }
  395. $this->connection->update($table)
  396. ->expression($field, $expression, $arguments)
  397. ->execute();
  398. }
  399. elseif (isset($spec['initial'])) {
  400. $this->connection->update($table)
  401. ->fields([$field => $spec['initial']])
  402. ->execute();
  403. }
  404. if ($fixnull) {
  405. $spec['not null'] = TRUE;
  406. $this->changeField($table, $field, $field, $spec);
  407. }
  408. }
  409. /**
  410. * {@inheritdoc}
  411. */
  412. public function dropField($table, $field) {
  413. if (!$this->fieldExists($table, $field)) {
  414. return FALSE;
  415. }
  416. // When dropping a field that is part of a composite primary key MySQL
  417. // automatically removes the field from the primary key, which can leave the
  418. // table in an invalid state. MariaDB 10.2.8 requires explicitly dropping
  419. // the primary key first for this reason. We perform this deletion
  420. // explicitly which also makes the behavior on both MySQL and MariaDB
  421. // consistent with PostgreSQL.
  422. // @see https://mariadb.com/kb/en/library/alter-table
  423. $primary_key = $this->findPrimaryKeyColumns($table);
  424. if ((count($primary_key) > 1) && in_array($field, $primary_key, TRUE)) {
  425. $this->dropPrimaryKey($table);
  426. }
  427. $this->connection->query('ALTER TABLE {' . $table . '} DROP `' . $field . '`');
  428. return TRUE;
  429. }
  430. /**
  431. * {@inheritdoc}
  432. */
  433. public function fieldSetDefault($table, $field, $default) {
  434. @trigger_error('fieldSetDefault() is deprecated in drupal:8.7.0 and will be removed before drupal:9.0.0. Instead, call ::changeField() passing a full field specification. See https://www.drupal.org/node/2999035', E_USER_DEPRECATED);
  435. if (!$this->fieldExists($table, $field)) {
  436. throw new SchemaObjectDoesNotExistException("Cannot set default value of field '$table.$field': field doesn't exist.");
  437. }
  438. $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` SET DEFAULT ' . $this->escapeDefaultValue($default));
  439. }
  440. /**
  441. * {@inheritdoc}
  442. */
  443. public function fieldSetNoDefault($table, $field) {
  444. @trigger_error('fieldSetNoDefault() is deprecated in drupal:8.7.0 and will be removed before drupal:9.0.0. Instead, call ::changeField() passing a full field specification. See https://www.drupal.org/node/2999035', E_USER_DEPRECATED);
  445. if (!$this->fieldExists($table, $field)) {
  446. throw new SchemaObjectDoesNotExistException("Cannot remove default value of field '$table.$field': field doesn't exist.");
  447. }
  448. $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` DROP DEFAULT');
  449. }
  450. /**
  451. * {@inheritdoc}
  452. */
  453. public function indexExists($table, $name) {
  454. // Returns one row for each column in the index. Result is string or FALSE.
  455. // Details at http://dev.mysql.com/doc/refman/5.0/en/show-index.html
  456. $row = $this->connection->query('SHOW INDEX FROM {' . $table . '} WHERE key_name = ' . $this->connection->quote($name))->fetchAssoc();
  457. return isset($row['Key_name']);
  458. }
  459. /**
  460. * {@inheritdoc}
  461. */
  462. public function addPrimaryKey($table, $fields) {
  463. if (!$this->tableExists($table)) {
  464. throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist.");
  465. }
  466. if ($this->indexExists($table, 'PRIMARY')) {
  467. throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists.");
  468. }
  469. $this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . $this->createKeySql($fields) . ')');
  470. }
  471. /**
  472. * {@inheritdoc}
  473. */
  474. public function dropPrimaryKey($table) {
  475. if (!$this->indexExists($table, 'PRIMARY')) {
  476. return FALSE;
  477. }
  478. $this->connection->query('ALTER TABLE {' . $table . '} DROP PRIMARY KEY');
  479. return TRUE;
  480. }
  481. /**
  482. * {@inheritdoc}
  483. */
  484. protected function findPrimaryKeyColumns($table) {
  485. if (!$this->tableExists($table)) {
  486. return FALSE;
  487. }
  488. $result = $this->connection->query("SHOW KEYS FROM {" . $table . "} WHERE Key_name = 'PRIMARY'")->fetchAllAssoc('Column_name');
  489. return array_keys($result);
  490. }
  491. /**
  492. * {@inheritdoc}
  493. */
  494. public function addUniqueKey($table, $name, $fields) {
  495. if (!$this->tableExists($table)) {
  496. throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist.");
  497. }
  498. if ($this->indexExists($table, $name)) {
  499. throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists.");
  500. }
  501. $this->connection->query('ALTER TABLE {' . $table . '} ADD UNIQUE KEY `' . $name . '` (' . $this->createKeySql($fields) . ')');
  502. }
  503. /**
  504. * {@inheritdoc}
  505. */
  506. public function dropUniqueKey($table, $name) {
  507. if (!$this->indexExists($table, $name)) {
  508. return FALSE;
  509. }
  510. $this->connection->query('ALTER TABLE {' . $table . '} DROP KEY `' . $name . '`');
  511. return TRUE;
  512. }
  513. /**
  514. * {@inheritdoc}
  515. */
  516. public function addIndex($table, $name, $fields, array $spec) {
  517. if (!$this->tableExists($table)) {
  518. throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist.");
  519. }
  520. if ($this->indexExists($table, $name)) {
  521. throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists.");
  522. }
  523. $spec['indexes'][$name] = $fields;
  524. $indexes = $this->getNormalizedIndexes($spec);
  525. $this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($indexes[$name]) . ')');
  526. }
  527. /**
  528. * {@inheritdoc}
  529. */
  530. public function dropIndex($table, $name) {
  531. if (!$this->indexExists($table, $name)) {
  532. return FALSE;
  533. }
  534. $this->connection->query('ALTER TABLE {' . $table . '} DROP INDEX `' . $name . '`');
  535. return TRUE;
  536. }
  537. /**
  538. * {@inheritdoc}
  539. */
  540. protected function introspectIndexSchema($table) {
  541. if (!$this->tableExists($table)) {
  542. throw new SchemaObjectDoesNotExistException("The table $table doesn't exist.");
  543. }
  544. $index_schema = [
  545. 'primary key' => [],
  546. 'unique keys' => [],
  547. 'indexes' => [],
  548. ];
  549. $result = $this->connection->query('SHOW INDEX FROM {' . $table . '}')->fetchAll();
  550. foreach ($result as $row) {
  551. if ($row->Key_name === 'PRIMARY') {
  552. $index_schema['primary key'][] = $row->Column_name;
  553. }
  554. elseif ($row->Non_unique == 0) {
  555. $index_schema['unique keys'][$row->Key_name][] = $row->Column_name;
  556. }
  557. else {
  558. $index_schema['indexes'][$row->Key_name][] = $row->Column_name;
  559. }
  560. }
  561. return $index_schema;
  562. }
  563. /**
  564. * {@inheritdoc}
  565. */
  566. public function changeField($table, $field, $field_new, $spec, $keys_new = []) {
  567. if (!$this->fieldExists($table, $field)) {
  568. throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist.");
  569. }
  570. if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
  571. throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists.");
  572. }
  573. if (isset($keys_new['primary key']) && in_array($field_new, $keys_new['primary key'], TRUE)) {
  574. $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field_new => $spec]);
  575. }
  576. $sql = 'ALTER TABLE {' . $table . '} CHANGE `' . $field . '` ' . $this->createFieldSql($field_new, $this->processField($spec));
  577. if ($keys_sql = $this->createKeysSql($keys_new)) {
  578. $sql .= ', ADD ' . implode(', ADD ', $keys_sql);
  579. }
  580. $this->connection->query($sql);
  581. }
  582. /**
  583. * {@inheritdoc}
  584. */
  585. public function prepareComment($comment, $length = NULL) {
  586. // Truncate comment to maximum comment length.
  587. if (isset($length)) {
  588. // Add table prefixes before truncating.
  589. $comment = Unicode::truncate($this->connection->prefixTables($comment), $length, TRUE, TRUE);
  590. }
  591. // Remove semicolons to avoid triggering multi-statement check.
  592. $comment = strtr($comment, [';' => '.']);
  593. return $this->connection->quote($comment);
  594. }
  595. /**
  596. * Retrieve a table or column comment.
  597. */
  598. public function getComment($table, $column = NULL) {
  599. $condition = $this->buildTableNameCondition($table);
  600. if (isset($column)) {
  601. $condition->condition('column_name', $column);
  602. $condition->compile($this->connection, $this);
  603. // Don't use {} around information_schema.columns table.
  604. return $this->connection->query("SELECT column_comment AS column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
  605. }
  606. $condition->compile($this->connection, $this);
  607. // Don't use {} around information_schema.tables table.
  608. $comment = $this->connection->query("SELECT table_comment AS table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
  609. // Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379
  610. return preg_replace('/; InnoDB free:.*$/', '', $comment);
  611. }
  612. /**
  613. * {@inheritdoc}
  614. */
  615. public function tableExists($table) {
  616. // The information_schema table is very slow to query under MySQL 5.0.
  617. // Instead, we try to select from the table in question. If it fails,
  618. // the most likely reason is that it does not exist. That is dramatically
  619. // faster than using information_schema.
  620. // @link http://bugs.mysql.com/bug.php?id=19588
  621. // @todo This override should be removed once we require a version of MySQL
  622. // that has that bug fixed.
  623. try {
  624. $this->connection->queryRange("SELECT 1 FROM {" . $table . "}", 0, 1);
  625. return TRUE;
  626. }
  627. catch (\Exception $e) {
  628. return FALSE;
  629. }
  630. }
  631. /**
  632. * {@inheritdoc}
  633. */
  634. public function fieldExists($table, $column) {
  635. // The information_schema table is very slow to query under MySQL 5.0.
  636. // Instead, we try to select from the table and field in question. If it
  637. // fails, the most likely reason is that it does not exist. That is
  638. // dramatically faster than using information_schema.
  639. // @link http://bugs.mysql.com/bug.php?id=19588
  640. // @todo This override should be removed once we require a version of MySQL
  641. // that has that bug fixed.
  642. try {
  643. $this->connection->queryRange("SELECT $column FROM {" . $table . "}", 0, 1);
  644. return TRUE;
  645. }
  646. catch (\Exception $e) {
  647. return FALSE;
  648. }
  649. }
  650. }
  651. /**
  652. * @} End of "addtogroup schemaapi".
  653. */