MysqlSchema.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Database\Schema;
  16. use Cake\Database\Exception;
  17. use Cake\Database\Schema\TableSchema;
  18. /**
  19. * Schema generation/reflection features for MySQL
  20. */
  21. class MysqlSchema extends BaseSchema
  22. {
  23. /**
  24. * The driver instance being used.
  25. *
  26. * @var \Cake\Database\Driver\Mysql
  27. */
  28. protected $_driver;
  29. /**
  30. * {@inheritDoc}
  31. */
  32. public function listTablesSql($config)
  33. {
  34. return ['SHOW TABLES FROM ' . $this->_driver->quoteIdentifier($config['database']), []];
  35. }
  36. /**
  37. * {@inheritDoc}
  38. */
  39. public function describeColumnSql($tableName, $config)
  40. {
  41. return ['SHOW FULL COLUMNS FROM ' . $this->_driver->quoteIdentifier($tableName), []];
  42. }
  43. /**
  44. * {@inheritDoc}
  45. */
  46. public function describeIndexSql($tableName, $config)
  47. {
  48. return ['SHOW INDEXES FROM ' . $this->_driver->quoteIdentifier($tableName), []];
  49. }
  50. /**
  51. * {@inheritDoc}
  52. */
  53. public function describeOptionsSql($tableName, $config)
  54. {
  55. return ['SHOW TABLE STATUS WHERE Name = ?', [$tableName]];
  56. }
  57. /**
  58. * {@inheritDoc}
  59. */
  60. public function convertOptionsDescription(TableSchema $schema, $row)
  61. {
  62. $schema->setOptions([
  63. 'engine' => $row['Engine'],
  64. 'collation' => $row['Collation'],
  65. ]);
  66. }
  67. /**
  68. * Convert a MySQL column type into an abstract type.
  69. *
  70. * The returned type will be a type that Cake\Database\Type can handle.
  71. *
  72. * @param string $column The column type + length
  73. * @return array Array of column information.
  74. * @throws \Cake\Database\Exception When column type cannot be parsed.
  75. */
  76. protected function _convertColumn($column)
  77. {
  78. preg_match('/([a-z]+)(?:\(([0-9,]+)\))?\s*([a-z]+)?/i', $column, $matches);
  79. if (empty($matches)) {
  80. throw new Exception(sprintf('Unable to parse column type from "%s"', $column));
  81. }
  82. $col = strtolower($matches[1]);
  83. $length = $precision = null;
  84. if (isset($matches[2])) {
  85. $length = $matches[2];
  86. if (strpos($matches[2], ',') !== false) {
  87. list($length, $precision) = explode(',', $length);
  88. }
  89. $length = (int)$length;
  90. $precision = (int)$precision;
  91. }
  92. if (in_array($col, ['date', 'time', 'datetime', 'timestamp'])) {
  93. return ['type' => $col, 'length' => null];
  94. }
  95. if (($col === 'tinyint' && $length === 1) || $col === 'boolean') {
  96. return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null];
  97. }
  98. $unsigned = (isset($matches[3]) && strtolower($matches[3]) === 'unsigned');
  99. if (strpos($col, 'bigint') !== false || $col === 'bigint') {
  100. return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => $length, 'unsigned' => $unsigned];
  101. }
  102. if ($col === 'tinyint') {
  103. return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => $length, 'unsigned' => $unsigned];
  104. }
  105. if ($col === 'smallint') {
  106. return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => $length, 'unsigned' => $unsigned];
  107. }
  108. if (in_array($col, ['int', 'integer', 'mediumint'])) {
  109. return ['type' => TableSchema::TYPE_INTEGER, 'length' => $length, 'unsigned' => $unsigned];
  110. }
  111. if ($col === 'char' && $length === 36) {
  112. return ['type' => TableSchema::TYPE_UUID, 'length' => null];
  113. }
  114. if ($col === 'char') {
  115. return ['type' => TableSchema::TYPE_STRING, 'fixed' => true, 'length' => $length];
  116. }
  117. if (strpos($col, 'char') !== false) {
  118. return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
  119. }
  120. if (strpos($col, 'text') !== false) {
  121. $lengthName = substr($col, 0, -4);
  122. $length = isset(TableSchema::$columnLengths[$lengthName]) ? TableSchema::$columnLengths[$lengthName] : null;
  123. return ['type' => TableSchema::TYPE_TEXT, 'length' => $length];
  124. }
  125. if ($col === 'binary' && $length === 16) {
  126. return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null];
  127. }
  128. if (strpos($col, 'blob') !== false || in_array($col, ['binary', 'varbinary'])) {
  129. $lengthName = substr($col, 0, -4);
  130. $length = isset(TableSchema::$columnLengths[$lengthName]) ? TableSchema::$columnLengths[$lengthName] : $length;
  131. return ['type' => TableSchema::TYPE_BINARY, 'length' => $length];
  132. }
  133. if (strpos($col, 'float') !== false || strpos($col, 'double') !== false) {
  134. return [
  135. 'type' => TableSchema::TYPE_FLOAT,
  136. 'length' => $length,
  137. 'precision' => $precision,
  138. 'unsigned' => $unsigned
  139. ];
  140. }
  141. if (strpos($col, 'decimal') !== false) {
  142. return [
  143. 'type' => TableSchema::TYPE_DECIMAL,
  144. 'length' => $length,
  145. 'precision' => $precision,
  146. 'unsigned' => $unsigned
  147. ];
  148. }
  149. if (strpos($col, 'json') !== false) {
  150. return ['type' => TableSchema::TYPE_JSON, 'length' => null];
  151. }
  152. return ['type' => TableSchema::TYPE_STRING, 'length' => null];
  153. }
  154. /**
  155. * {@inheritDoc}
  156. */
  157. public function convertColumnDescription(TableSchema $schema, $row)
  158. {
  159. $field = $this->_convertColumn($row['Type']);
  160. $field += [
  161. 'null' => $row['Null'] === 'YES',
  162. 'default' => $row['Default'],
  163. 'collate' => $row['Collation'],
  164. 'comment' => $row['Comment'],
  165. ];
  166. if (isset($row['Extra']) && $row['Extra'] === 'auto_increment') {
  167. $field['autoIncrement'] = true;
  168. }
  169. $schema->addColumn($row['Field'], $field);
  170. }
  171. /**
  172. * {@inheritDoc}
  173. */
  174. public function convertIndexDescription(TableSchema $schema, $row)
  175. {
  176. $type = null;
  177. $columns = $length = [];
  178. $name = $row['Key_name'];
  179. if ($name === 'PRIMARY') {
  180. $name = $type = TableSchema::CONSTRAINT_PRIMARY;
  181. }
  182. $columns[] = $row['Column_name'];
  183. if ($row['Index_type'] === 'FULLTEXT') {
  184. $type = TableSchema::INDEX_FULLTEXT;
  185. } elseif ($row['Non_unique'] == 0 && $type !== 'primary') {
  186. $type = TableSchema::CONSTRAINT_UNIQUE;
  187. } elseif ($type !== 'primary') {
  188. $type = TableSchema::INDEX_INDEX;
  189. }
  190. if (!empty($row['Sub_part'])) {
  191. $length[$row['Column_name']] = $row['Sub_part'];
  192. }
  193. $isIndex = (
  194. $type === TableSchema::INDEX_INDEX ||
  195. $type === TableSchema::INDEX_FULLTEXT
  196. );
  197. if ($isIndex) {
  198. $existing = $schema->getIndex($name);
  199. } else {
  200. $existing = $schema->getConstraint($name);
  201. }
  202. // MySQL multi column indexes come back as multiple rows.
  203. if (!empty($existing)) {
  204. $columns = array_merge($existing['columns'], $columns);
  205. $length = array_merge($existing['length'], $length);
  206. }
  207. if ($isIndex) {
  208. $schema->addIndex($name, [
  209. 'type' => $type,
  210. 'columns' => $columns,
  211. 'length' => $length
  212. ]);
  213. } else {
  214. $schema->addConstraint($name, [
  215. 'type' => $type,
  216. 'columns' => $columns,
  217. 'length' => $length
  218. ]);
  219. }
  220. }
  221. /**
  222. * {@inheritDoc}
  223. */
  224. public function describeForeignKeySql($tableName, $config)
  225. {
  226. $sql = 'SELECT * FROM information_schema.key_column_usage AS kcu
  227. INNER JOIN information_schema.referential_constraints AS rc
  228. ON (
  229. kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
  230. AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
  231. )
  232. WHERE kcu.TABLE_SCHEMA = ? AND kcu.TABLE_NAME = ? AND rc.TABLE_NAME = ?';
  233. return [$sql, [$config['database'], $tableName, $tableName]];
  234. }
  235. /**
  236. * {@inheritDoc}
  237. */
  238. public function convertForeignKeyDescription(TableSchema $schema, $row)
  239. {
  240. $data = [
  241. 'type' => TableSchema::CONSTRAINT_FOREIGN,
  242. 'columns' => [$row['COLUMN_NAME']],
  243. 'references' => [$row['REFERENCED_TABLE_NAME'], $row['REFERENCED_COLUMN_NAME']],
  244. 'update' => $this->_convertOnClause($row['UPDATE_RULE']),
  245. 'delete' => $this->_convertOnClause($row['DELETE_RULE']),
  246. ];
  247. $name = $row['CONSTRAINT_NAME'];
  248. $schema->addConstraint($name, $data);
  249. }
  250. /**
  251. * {@inheritDoc}
  252. */
  253. public function truncateTableSql(TableSchema $schema)
  254. {
  255. return [sprintf('TRUNCATE TABLE `%s`', $schema->name())];
  256. }
  257. /**
  258. * {@inheritDoc}
  259. */
  260. public function createTableSql(TableSchema $schema, $columns, $constraints, $indexes)
  261. {
  262. $content = implode(",\n", array_merge($columns, $constraints, $indexes));
  263. $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' ';
  264. $content = sprintf("CREATE%sTABLE `%s` (\n%s\n)", $temporary, $schema->name(), $content);
  265. $options = $schema->getOptions();
  266. if (isset($options['engine'])) {
  267. $content .= sprintf(' ENGINE=%s', $options['engine']);
  268. }
  269. if (isset($options['charset'])) {
  270. $content .= sprintf(' DEFAULT CHARSET=%s', $options['charset']);
  271. }
  272. if (isset($options['collate'])) {
  273. $content .= sprintf(' COLLATE=%s', $options['collate']);
  274. }
  275. return [$content];
  276. }
  277. /**
  278. * {@inheritDoc}
  279. */
  280. public function columnSql(TableSchema $schema, $name)
  281. {
  282. $data = $schema->getColumn($name);
  283. $out = $this->_driver->quoteIdentifier($name);
  284. $nativeJson = $this->_driver->supportsNativeJson();
  285. $typeMap = [
  286. TableSchema::TYPE_TINYINTEGER => ' TINYINT',
  287. TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
  288. TableSchema::TYPE_INTEGER => ' INTEGER',
  289. TableSchema::TYPE_BIGINTEGER => ' BIGINT',
  290. TableSchema::TYPE_BINARY_UUID => ' BINARY(16)',
  291. TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
  292. TableSchema::TYPE_FLOAT => ' FLOAT',
  293. TableSchema::TYPE_DECIMAL => ' DECIMAL',
  294. TableSchema::TYPE_DATE => ' DATE',
  295. TableSchema::TYPE_TIME => ' TIME',
  296. TableSchema::TYPE_DATETIME => ' DATETIME',
  297. TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP',
  298. TableSchema::TYPE_UUID => ' CHAR(36)',
  299. TableSchema::TYPE_JSON => $nativeJson ? ' JSON' : ' LONGTEXT'
  300. ];
  301. $specialMap = [
  302. 'string' => true,
  303. 'text' => true,
  304. 'binary' => true,
  305. ];
  306. if (isset($typeMap[$data['type']])) {
  307. $out .= $typeMap[$data['type']];
  308. }
  309. if (isset($specialMap[$data['type']])) {
  310. switch ($data['type']) {
  311. case TableSchema::TYPE_STRING:
  312. $out .= !empty($data['fixed']) ? ' CHAR' : ' VARCHAR';
  313. if (!isset($data['length'])) {
  314. $data['length'] = 255;
  315. }
  316. break;
  317. case TableSchema::TYPE_TEXT:
  318. $isKnownLength = in_array($data['length'], TableSchema::$columnLengths);
  319. if (empty($data['length']) || !$isKnownLength) {
  320. $out .= ' TEXT';
  321. break;
  322. }
  323. if ($isKnownLength) {
  324. $length = array_search($data['length'], TableSchema::$columnLengths);
  325. $out .= ' ' . strtoupper($length) . 'TEXT';
  326. }
  327. break;
  328. case TableSchema::TYPE_BINARY:
  329. $isKnownLength = in_array($data['length'], TableSchema::$columnLengths);
  330. if ($isKnownLength) {
  331. $length = array_search($data['length'], TableSchema::$columnLengths);
  332. $out .= ' ' . strtoupper($length) . 'BLOB';
  333. break;
  334. }
  335. if (empty($data['length'])) {
  336. $out .= ' BLOB';
  337. break;
  338. }
  339. if ($data['length'] > 2) {
  340. $out .= ' VARBINARY(' . $data['length'] . ')';
  341. } else {
  342. $out .= ' BINARY(' . $data['length'] . ')';
  343. }
  344. break;
  345. }
  346. }
  347. $hasLength = [
  348. TableSchema::TYPE_INTEGER,
  349. TableSchema::TYPE_SMALLINTEGER,
  350. TableSchema::TYPE_TINYINTEGER,
  351. TableSchema::TYPE_STRING
  352. ];
  353. if (in_array($data['type'], $hasLength, true) && isset($data['length'])) {
  354. $out .= '(' . (int)$data['length'] . ')';
  355. }
  356. $hasPrecision = [TableSchema::TYPE_FLOAT, TableSchema::TYPE_DECIMAL];
  357. if (in_array($data['type'], $hasPrecision, true) &&
  358. (isset($data['length']) || isset($data['precision']))
  359. ) {
  360. $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
  361. }
  362. $hasUnsigned = [
  363. TableSchema::TYPE_TINYINTEGER,
  364. TableSchema::TYPE_SMALLINTEGER,
  365. TableSchema::TYPE_INTEGER,
  366. TableSchema::TYPE_BIGINTEGER,
  367. TableSchema::TYPE_FLOAT,
  368. TableSchema::TYPE_DECIMAL
  369. ];
  370. if (in_array($data['type'], $hasUnsigned, true) &&
  371. isset($data['unsigned']) && $data['unsigned'] === true
  372. ) {
  373. $out .= ' UNSIGNED';
  374. }
  375. $hasCollate = [
  376. TableSchema::TYPE_TEXT,
  377. TableSchema::TYPE_STRING,
  378. ];
  379. if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
  380. $out .= ' COLLATE ' . $data['collate'];
  381. }
  382. if (isset($data['null']) && $data['null'] === false) {
  383. $out .= ' NOT NULL';
  384. }
  385. $addAutoIncrement = (
  386. [$name] == (array)$schema->primaryKey() &&
  387. !$schema->hasAutoincrement() &&
  388. !isset($data['autoIncrement'])
  389. );
  390. if (in_array($data['type'], [TableSchema::TYPE_INTEGER, TableSchema::TYPE_BIGINTEGER]) &&
  391. ($data['autoIncrement'] === true || $addAutoIncrement)
  392. ) {
  393. $out .= ' AUTO_INCREMENT';
  394. }
  395. if (isset($data['null']) && $data['null'] === true && $data['type'] === TableSchema::TYPE_TIMESTAMP) {
  396. $out .= ' NULL';
  397. unset($data['default']);
  398. }
  399. if (isset($data['default']) &&
  400. in_array($data['type'], [TableSchema::TYPE_TIMESTAMP, TableSchema::TYPE_DATETIME]) &&
  401. in_array(strtolower($data['default']), ['current_timestamp', 'current_timestamp()'])
  402. ) {
  403. $out .= ' DEFAULT CURRENT_TIMESTAMP';
  404. unset($data['default']);
  405. }
  406. if (isset($data['default'])) {
  407. $out .= ' DEFAULT ' . $this->_driver->schemaValue($data['default']);
  408. unset($data['default']);
  409. }
  410. if (isset($data['comment']) && $data['comment'] !== '') {
  411. $out .= ' COMMENT ' . $this->_driver->schemaValue($data['comment']);
  412. }
  413. return $out;
  414. }
  415. /**
  416. * {@inheritDoc}
  417. */
  418. public function constraintSql(TableSchema $schema, $name)
  419. {
  420. $data = $schema->getConstraint($name);
  421. if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
  422. $columns = array_map(
  423. [$this->_driver, 'quoteIdentifier'],
  424. $data['columns']
  425. );
  426. return sprintf('PRIMARY KEY (%s)', implode(', ', $columns));
  427. }
  428. $out = '';
  429. if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
  430. $out = 'UNIQUE KEY ';
  431. }
  432. if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
  433. $out = 'CONSTRAINT ';
  434. }
  435. $out .= $this->_driver->quoteIdentifier($name);
  436. return $this->_keySql($out, $data);
  437. }
  438. /**
  439. * {@inheritDoc}
  440. */
  441. public function addConstraintSql(TableSchema $schema)
  442. {
  443. $sqlPattern = 'ALTER TABLE %s ADD %s;';
  444. $sql = [];
  445. foreach ($schema->constraints() as $name) {
  446. $constraint = $schema->getConstraint($name);
  447. if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
  448. $tableName = $this->_driver->quoteIdentifier($schema->name());
  449. $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name));
  450. }
  451. }
  452. return $sql;
  453. }
  454. /**
  455. * {@inheritDoc}
  456. */
  457. public function dropConstraintSql(TableSchema $schema)
  458. {
  459. $sqlPattern = 'ALTER TABLE %s DROP FOREIGN KEY %s;';
  460. $sql = [];
  461. foreach ($schema->constraints() as $name) {
  462. $constraint = $schema->getConstraint($name);
  463. if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
  464. $tableName = $this->_driver->quoteIdentifier($schema->name());
  465. $constraintName = $this->_driver->quoteIdentifier($name);
  466. $sql[] = sprintf($sqlPattern, $tableName, $constraintName);
  467. }
  468. }
  469. return $sql;
  470. }
  471. /**
  472. * {@inheritDoc}
  473. */
  474. public function indexSql(TableSchema $schema, $name)
  475. {
  476. $data = $schema->getIndex($name);
  477. $out = '';
  478. if ($data['type'] === TableSchema::INDEX_INDEX) {
  479. $out = 'KEY ';
  480. }
  481. if ($data['type'] === TableSchema::INDEX_FULLTEXT) {
  482. $out = 'FULLTEXT KEY ';
  483. }
  484. $out .= $this->_driver->quoteIdentifier($name);
  485. return $this->_keySql($out, $data);
  486. }
  487. /**
  488. * Helper method for generating key SQL snippets.
  489. *
  490. * @param string $prefix The key prefix
  491. * @param array $data Key data.
  492. * @return string
  493. */
  494. protected function _keySql($prefix, $data)
  495. {
  496. $columns = array_map(
  497. [$this->_driver, 'quoteIdentifier'],
  498. $data['columns']
  499. );
  500. foreach ($data['columns'] as $i => $column) {
  501. if (isset($data['length'][$column])) {
  502. $columns[$i] .= sprintf('(%d)', $data['length'][$column]);
  503. }
  504. }
  505. if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
  506. return $prefix . sprintf(
  507. ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s',
  508. implode(', ', $columns),
  509. $this->_driver->quoteIdentifier($data['references'][0]),
  510. $this->_convertConstraintColumns($data['references'][1]),
  511. $this->_foreignOnClause($data['update']),
  512. $this->_foreignOnClause($data['delete'])
  513. );
  514. }
  515. return $prefix . ' (' . implode(', ', $columns) . ')';
  516. }
  517. }