SqliteSchema.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  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 management/reflection features for Sqlite
  20. */
  21. class SqliteSchema extends BaseSchema
  22. {
  23. /**
  24. * Array containing the foreign keys constraints names
  25. * Necessary for composite foreign keys to be handled
  26. *
  27. * @var array
  28. */
  29. protected $_constraintsIdMap = [];
  30. /**
  31. * Whether there is any table in this connection to SQLite containing sequences.
  32. *
  33. * @var bool
  34. */
  35. protected $_hasSequences;
  36. /**
  37. * Convert a column definition to the abstract types.
  38. *
  39. * The returned type will be a type that
  40. * Cake\Database\Type can handle.
  41. *
  42. * @param string $column The column type + length
  43. * @throws \Cake\Database\Exception when unable to parse column type
  44. * @return array Array of column information.
  45. */
  46. protected function _convertColumn($column)
  47. {
  48. preg_match('/(unsigned)?\s*([a-z]+)(?:\(([0-9,]+)\))?/i', $column, $matches);
  49. if (empty($matches)) {
  50. throw new Exception(sprintf('Unable to parse column type from "%s"', $column));
  51. }
  52. $unsigned = false;
  53. if (strtolower($matches[1]) === 'unsigned') {
  54. $unsigned = true;
  55. }
  56. $col = strtolower($matches[2]);
  57. $length = null;
  58. if (isset($matches[3])) {
  59. $length = (int)$matches[3];
  60. }
  61. if ($col === 'bigint') {
  62. return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => $length, 'unsigned' => $unsigned];
  63. }
  64. if ($col == 'smallint') {
  65. return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => $length, 'unsigned' => $unsigned];
  66. }
  67. if ($col == 'tinyint') {
  68. return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => $length, 'unsigned' => $unsigned];
  69. }
  70. if (strpos($col, 'int') !== false) {
  71. return ['type' => TableSchema::TYPE_INTEGER, 'length' => $length, 'unsigned' => $unsigned];
  72. }
  73. if (strpos($col, 'decimal') !== false) {
  74. return ['type' => TableSchema::TYPE_DECIMAL, 'length' => null, 'unsigned' => $unsigned];
  75. }
  76. if (in_array($col, ['float', 'real', 'double'])) {
  77. return ['type' => TableSchema::TYPE_FLOAT, 'length' => null, 'unsigned' => $unsigned];
  78. }
  79. if (strpos($col, 'boolean') !== false) {
  80. return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null];
  81. }
  82. if ($col === 'char' && $length === 36) {
  83. return ['type' => TableSchema::TYPE_UUID, 'length' => null];
  84. }
  85. if ($col === 'char') {
  86. return ['type' => TableSchema::TYPE_STRING, 'fixed' => true, 'length' => $length];
  87. }
  88. if (strpos($col, 'char') !== false) {
  89. return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
  90. }
  91. if ($col === 'binary' && $length === 16) {
  92. return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null];
  93. }
  94. if (in_array($col, ['blob', 'clob', 'binary', 'varbinary'])) {
  95. return ['type' => TableSchema::TYPE_BINARY, 'length' => $length];
  96. }
  97. if (in_array($col, ['date', 'time', 'timestamp', 'datetime'])) {
  98. return ['type' => $col, 'length' => null];
  99. }
  100. return ['type' => TableSchema::TYPE_TEXT, 'length' => null];
  101. }
  102. /**
  103. * {@inheritDoc}
  104. */
  105. public function listTablesSql($config)
  106. {
  107. return [
  108. 'SELECT name FROM sqlite_master WHERE type="table" ' .
  109. 'AND name != "sqlite_sequence" ORDER BY name',
  110. []
  111. ];
  112. }
  113. /**
  114. * {@inheritDoc}
  115. */
  116. public function describeColumnSql($tableName, $config)
  117. {
  118. $sql = sprintf(
  119. 'PRAGMA table_info(%s)',
  120. $this->_driver->quoteIdentifier($tableName)
  121. );
  122. return [$sql, []];
  123. }
  124. /**
  125. * {@inheritDoc}
  126. */
  127. public function convertColumnDescription(TableSchema $schema, $row)
  128. {
  129. $field = $this->_convertColumn($row['type']);
  130. $field += [
  131. 'null' => !$row['notnull'],
  132. 'default' => $this->_defaultValue($row['dflt_value']),
  133. ];
  134. $primary = $schema->getConstraint('primary');
  135. if ($row['pk'] && empty($primary)) {
  136. $field['null'] = false;
  137. $field['autoIncrement'] = true;
  138. }
  139. // SQLite does not support autoincrement on composite keys.
  140. if ($row['pk'] && !empty($primary)) {
  141. $existingColumn = $primary['columns'][0];
  142. $schema->addColumn($existingColumn, ['autoIncrement' => null] + $schema->getColumn($existingColumn));
  143. }
  144. $schema->addColumn($row['name'], $field);
  145. if ($row['pk']) {
  146. $constraint = (array)$schema->getConstraint('primary') + [
  147. 'type' => TableSchema::CONSTRAINT_PRIMARY,
  148. 'columns' => []
  149. ];
  150. $constraint['columns'] = array_merge($constraint['columns'], [$row['name']]);
  151. $schema->addConstraint('primary', $constraint);
  152. }
  153. }
  154. /**
  155. * Manipulate the default value.
  156. *
  157. * Sqlite includes quotes and bared NULLs in default values.
  158. * We need to remove those.
  159. *
  160. * @param string|null $default The default value.
  161. * @return string|null
  162. */
  163. protected function _defaultValue($default)
  164. {
  165. if ($default === 'NULL') {
  166. return null;
  167. }
  168. // Remove quotes
  169. if (preg_match("/^'(.*)'$/", $default, $matches)) {
  170. return str_replace("''", "'", $matches[1]);
  171. }
  172. return $default;
  173. }
  174. /**
  175. * {@inheritDoc}
  176. */
  177. public function describeIndexSql($tableName, $config)
  178. {
  179. $sql = sprintf(
  180. 'PRAGMA index_list(%s)',
  181. $this->_driver->quoteIdentifier($tableName)
  182. );
  183. return [$sql, []];
  184. }
  185. /**
  186. * {@inheritDoc}
  187. *
  188. * Since SQLite does not have a way to get metadata about all indexes at once,
  189. * additional queries are done here. Sqlite constraint names are not
  190. * stable, and the names for constraints will not match those used to create
  191. * the table. This is a limitation in Sqlite's metadata features.
  192. *
  193. */
  194. public function convertIndexDescription(TableSchema $schema, $row)
  195. {
  196. $sql = sprintf(
  197. 'PRAGMA index_info(%s)',
  198. $this->_driver->quoteIdentifier($row['name'])
  199. );
  200. $statement = $this->_driver->prepare($sql);
  201. $statement->execute();
  202. $columns = [];
  203. foreach ($statement->fetchAll('assoc') as $column) {
  204. $columns[] = $column['name'];
  205. }
  206. $statement->closeCursor();
  207. if ($row['unique']) {
  208. $schema->addConstraint($row['name'], [
  209. 'type' => TableSchema::CONSTRAINT_UNIQUE,
  210. 'columns' => $columns
  211. ]);
  212. } else {
  213. $schema->addIndex($row['name'], [
  214. 'type' => TableSchema::INDEX_INDEX,
  215. 'columns' => $columns
  216. ]);
  217. }
  218. }
  219. /**
  220. * {@inheritDoc}
  221. */
  222. public function describeForeignKeySql($tableName, $config)
  223. {
  224. $sql = sprintf('PRAGMA foreign_key_list(%s)', $this->_driver->quoteIdentifier($tableName));
  225. return [$sql, []];
  226. }
  227. /**
  228. * {@inheritDoc}
  229. */
  230. public function convertForeignKeyDescription(TableSchema $schema, $row)
  231. {
  232. $name = $row['from'] . '_fk';
  233. $update = isset($row['on_update']) ? $row['on_update'] : '';
  234. $delete = isset($row['on_delete']) ? $row['on_delete'] : '';
  235. $data = [
  236. 'type' => TableSchema::CONSTRAINT_FOREIGN,
  237. 'columns' => [$row['from']],
  238. 'references' => [$row['table'], $row['to']],
  239. 'update' => $this->_convertOnClause($update),
  240. 'delete' => $this->_convertOnClause($delete),
  241. ];
  242. if (isset($this->_constraintsIdMap[$schema->name()][$row['id']])) {
  243. $name = $this->_constraintsIdMap[$schema->name()][$row['id']];
  244. } else {
  245. $this->_constraintsIdMap[$schema->name()][$row['id']] = $name;
  246. }
  247. $schema->addConstraint($name, $data);
  248. }
  249. /**
  250. * {@inheritDoc}
  251. *
  252. * @throws \Cake\Database\Exception when the column type is unknown
  253. */
  254. public function columnSql(TableSchema $schema, $name)
  255. {
  256. $data = $schema->getColumn($name);
  257. $typeMap = [
  258. TableSchema::TYPE_BINARY_UUID => ' BINARY(16)',
  259. TableSchema::TYPE_UUID => ' CHAR(36)',
  260. TableSchema::TYPE_TINYINTEGER => ' TINYINT',
  261. TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
  262. TableSchema::TYPE_INTEGER => ' INTEGER',
  263. TableSchema::TYPE_BIGINTEGER => ' BIGINT',
  264. TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
  265. TableSchema::TYPE_FLOAT => ' FLOAT',
  266. TableSchema::TYPE_DECIMAL => ' DECIMAL',
  267. TableSchema::TYPE_DATE => ' DATE',
  268. TableSchema::TYPE_TIME => ' TIME',
  269. TableSchema::TYPE_DATETIME => ' DATETIME',
  270. TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP',
  271. TableSchema::TYPE_JSON => ' TEXT'
  272. ];
  273. $out = $this->_driver->quoteIdentifier($name);
  274. $hasUnsigned = [
  275. TableSchema::TYPE_TINYINTEGER,
  276. TableSchema::TYPE_SMALLINTEGER,
  277. TableSchema::TYPE_INTEGER,
  278. TableSchema::TYPE_BIGINTEGER,
  279. TableSchema::TYPE_FLOAT,
  280. TableSchema::TYPE_DECIMAL
  281. ];
  282. if (in_array($data['type'], $hasUnsigned, true) &&
  283. isset($data['unsigned']) && $data['unsigned'] === true
  284. ) {
  285. if ($data['type'] !== TableSchema::TYPE_INTEGER || [$name] !== (array)$schema->primaryKey()) {
  286. $out .= ' UNSIGNED';
  287. }
  288. }
  289. if (isset($typeMap[$data['type']])) {
  290. $out .= $typeMap[$data['type']];
  291. }
  292. if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) {
  293. $out .= ' TEXT';
  294. }
  295. if ($data['type'] === TableSchema::TYPE_STRING ||
  296. ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] === TableSchema::LENGTH_TINY)
  297. ) {
  298. $out .= ' VARCHAR';
  299. if (isset($data['length'])) {
  300. $out .= '(' . (int)$data['length'] . ')';
  301. }
  302. }
  303. if ($data['type'] === TableSchema::TYPE_BINARY) {
  304. if (isset($data['length'])) {
  305. $out .= ' BLOB(' . (int)$data['length'] . ')';
  306. } else {
  307. $out .= ' BLOB';
  308. }
  309. }
  310. $integerTypes = [
  311. TableSchema::TYPE_TINYINTEGER,
  312. TableSchema::TYPE_SMALLINTEGER,
  313. TableSchema::TYPE_INTEGER,
  314. ];
  315. if (in_array($data['type'], $integerTypes, true) &&
  316. isset($data['length']) && [$name] !== (array)$schema->primaryKey()
  317. ) {
  318. $out .= '(' . (int)$data['length'] . ')';
  319. }
  320. $hasPrecision = [TableSchema::TYPE_FLOAT, TableSchema::TYPE_DECIMAL];
  321. if (in_array($data['type'], $hasPrecision, true) &&
  322. (isset($data['length']) || isset($data['precision']))
  323. ) {
  324. $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
  325. }
  326. if (isset($data['null']) && $data['null'] === false) {
  327. $out .= ' NOT NULL';
  328. }
  329. if ($data['type'] === TableSchema::TYPE_INTEGER && [$name] === (array)$schema->primaryKey()) {
  330. $out .= ' PRIMARY KEY AUTOINCREMENT';
  331. }
  332. if (isset($data['null']) && $data['null'] === true && $data['type'] === TableSchema::TYPE_TIMESTAMP) {
  333. $out .= ' DEFAULT NULL';
  334. }
  335. if (isset($data['default'])) {
  336. $out .= ' DEFAULT ' . $this->_driver->schemaValue($data['default']);
  337. }
  338. return $out;
  339. }
  340. /**
  341. * {@inheritDoc}
  342. *
  343. * Note integer primary keys will return ''. This is intentional as Sqlite requires
  344. * that integer primary keys be defined in the column definition.
  345. *
  346. */
  347. public function constraintSql(TableSchema $schema, $name)
  348. {
  349. $data = $schema->getConstraint($name);
  350. if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY &&
  351. count($data['columns']) === 1 &&
  352. $schema->getColumn($data['columns'][0])['type'] === TableSchema::TYPE_INTEGER
  353. ) {
  354. return '';
  355. }
  356. $clause = '';
  357. $type = '';
  358. if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
  359. $type = 'PRIMARY KEY';
  360. }
  361. if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
  362. $type = 'UNIQUE';
  363. }
  364. if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
  365. $type = 'FOREIGN KEY';
  366. $clause = sprintf(
  367. ' REFERENCES %s (%s) ON UPDATE %s ON DELETE %s',
  368. $this->_driver->quoteIdentifier($data['references'][0]),
  369. $this->_convertConstraintColumns($data['references'][1]),
  370. $this->_foreignOnClause($data['update']),
  371. $this->_foreignOnClause($data['delete'])
  372. );
  373. }
  374. $columns = array_map(
  375. [$this->_driver, 'quoteIdentifier'],
  376. $data['columns']
  377. );
  378. return sprintf(
  379. 'CONSTRAINT %s %s (%s)%s',
  380. $this->_driver->quoteIdentifier($name),
  381. $type,
  382. implode(', ', $columns),
  383. $clause
  384. );
  385. }
  386. /**
  387. * {@inheritDoc}
  388. *
  389. * SQLite can not properly handle adding a constraint to an existing table.
  390. * This method is no-op
  391. */
  392. public function addConstraintSql(TableSchema $schema)
  393. {
  394. return [];
  395. }
  396. /**
  397. * {@inheritDoc}
  398. *
  399. * SQLite can not properly handle dropping a constraint to an existing table.
  400. * This method is no-op
  401. */
  402. public function dropConstraintSql(TableSchema $schema)
  403. {
  404. return [];
  405. }
  406. /**
  407. * {@inheritDoc}
  408. */
  409. public function indexSql(TableSchema $schema, $name)
  410. {
  411. $data = $schema->getIndex($name);
  412. $columns = array_map(
  413. [$this->_driver, 'quoteIdentifier'],
  414. $data['columns']
  415. );
  416. return sprintf(
  417. 'CREATE INDEX %s ON %s (%s)',
  418. $this->_driver->quoteIdentifier($name),
  419. $this->_driver->quoteIdentifier($schema->name()),
  420. implode(', ', $columns)
  421. );
  422. }
  423. /**
  424. * {@inheritDoc}
  425. */
  426. public function createTableSql(TableSchema $schema, $columns, $constraints, $indexes)
  427. {
  428. $lines = array_merge($columns, $constraints);
  429. $content = implode(",\n", array_filter($lines));
  430. $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' ';
  431. $table = sprintf("CREATE%sTABLE \"%s\" (\n%s\n)", $temporary, $schema->name(), $content);
  432. $out = [$table];
  433. foreach ($indexes as $index) {
  434. $out[] = $index;
  435. }
  436. return $out;
  437. }
  438. /**
  439. * {@inheritDoc}
  440. */
  441. public function truncateTableSql(TableSchema $schema)
  442. {
  443. $name = $schema->name();
  444. $sql = [];
  445. if ($this->hasSequences()) {
  446. $sql[] = sprintf('DELETE FROM sqlite_sequence WHERE name="%s"', $name);
  447. }
  448. $sql[] = sprintf('DELETE FROM "%s"', $name);
  449. return $sql;
  450. }
  451. /**
  452. * Returns whether there is any table in this connection to SQLite containing
  453. * sequences
  454. *
  455. * @return bool
  456. */
  457. public function hasSequences()
  458. {
  459. $result = $this->_driver->prepare(
  460. 'SELECT 1 FROM sqlite_master WHERE name = "sqlite_sequence"'
  461. );
  462. $result->execute();
  463. $this->_hasSequences = (bool)$result->rowCount();
  464. $result->closeCursor();
  465. return $this->_hasSequences;
  466. }
  467. }