SqliteSchemaManager.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. <?php
  2. namespace Doctrine\DBAL\Schema;
  3. use Doctrine\DBAL\DriverManager;
  4. use Doctrine\DBAL\Exception;
  5. use Doctrine\DBAL\Types\StringType;
  6. use Doctrine\DBAL\Types\TextType;
  7. use Doctrine\DBAL\Types\Type;
  8. use function array_change_key_case;
  9. use function array_map;
  10. use function array_merge;
  11. use function array_reverse;
  12. use function array_values;
  13. use function explode;
  14. use function file_exists;
  15. use function preg_match;
  16. use function preg_match_all;
  17. use function preg_quote;
  18. use function preg_replace;
  19. use function rtrim;
  20. use function sprintf;
  21. use function str_replace;
  22. use function strpos;
  23. use function strtolower;
  24. use function trim;
  25. use function unlink;
  26. use function usort;
  27. use const CASE_LOWER;
  28. /**
  29. * Sqlite SchemaManager.
  30. */
  31. class SqliteSchemaManager extends AbstractSchemaManager
  32. {
  33. /**
  34. * {@inheritdoc}
  35. */
  36. public function dropDatabase($database)
  37. {
  38. if (! file_exists($database)) {
  39. return;
  40. }
  41. unlink($database);
  42. }
  43. /**
  44. * {@inheritdoc}
  45. */
  46. public function createDatabase($database)
  47. {
  48. $params = $this->_conn->getParams();
  49. $params['path'] = $database;
  50. unset($params['memory']);
  51. $conn = DriverManager::getConnection($params);
  52. $conn->connect();
  53. $conn->close();
  54. }
  55. /**
  56. * {@inheritdoc}
  57. */
  58. public function renameTable($name, $newName)
  59. {
  60. $tableDiff = new TableDiff($name);
  61. $tableDiff->fromTable = $this->listTableDetails($name);
  62. $tableDiff->newName = $newName;
  63. $this->alterTable($tableDiff);
  64. }
  65. /**
  66. * {@inheritdoc}
  67. */
  68. public function createForeignKey(ForeignKeyConstraint $foreignKey, $table)
  69. {
  70. $tableDiff = $this->getTableDiffForAlterForeignKey($table);
  71. $tableDiff->addedForeignKeys[] = $foreignKey;
  72. $this->alterTable($tableDiff);
  73. }
  74. /**
  75. * {@inheritdoc}
  76. */
  77. public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table)
  78. {
  79. $tableDiff = $this->getTableDiffForAlterForeignKey($table);
  80. $tableDiff->changedForeignKeys[] = $foreignKey;
  81. $this->alterTable($tableDiff);
  82. }
  83. /**
  84. * {@inheritdoc}
  85. */
  86. public function dropForeignKey($foreignKey, $table)
  87. {
  88. $tableDiff = $this->getTableDiffForAlterForeignKey($table);
  89. $tableDiff->removedForeignKeys[] = $foreignKey;
  90. $this->alterTable($tableDiff);
  91. }
  92. /**
  93. * {@inheritdoc}
  94. */
  95. public function listTableForeignKeys($table, $database = null)
  96. {
  97. if ($database === null) {
  98. $database = $this->_conn->getDatabase();
  99. }
  100. $sql = $this->_platform->getListTableForeignKeysSQL($table, $database);
  101. $tableForeignKeys = $this->_conn->fetchAllAssociative($sql);
  102. if (! empty($tableForeignKeys)) {
  103. $createSql = $this->getCreateTableSQL($table);
  104. if (
  105. $createSql !== null && preg_match_all(
  106. '#
  107. (?:CONSTRAINT\s+([^\s]+)\s+)?
  108. (?:FOREIGN\s+KEY[^\)]+\)\s*)?
  109. REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))?
  110. (?:
  111. [^,]*?
  112. (NOT\s+DEFERRABLE|DEFERRABLE)
  113. (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
  114. )?#isx',
  115. $createSql,
  116. $match
  117. )
  118. ) {
  119. $names = array_reverse($match[1]);
  120. $deferrable = array_reverse($match[2]);
  121. $deferred = array_reverse($match[3]);
  122. } else {
  123. $names = $deferrable = $deferred = [];
  124. }
  125. foreach ($tableForeignKeys as $key => $value) {
  126. $id = $value['id'];
  127. $tableForeignKeys[$key] = array_merge($tableForeignKeys[$key], [
  128. 'constraint_name' => isset($names[$id]) && $names[$id] !== '' ? $names[$id] : $id,
  129. 'deferrable' => isset($deferrable[$id]) && strtolower($deferrable[$id]) === 'deferrable',
  130. 'deferred' => isset($deferred[$id]) && strtolower($deferred[$id]) === 'deferred',
  131. ]);
  132. }
  133. }
  134. return $this->_getPortableTableForeignKeysList($tableForeignKeys);
  135. }
  136. /**
  137. * {@inheritdoc}
  138. */
  139. protected function _getPortableTableDefinition($table)
  140. {
  141. return $table['name'];
  142. }
  143. /**
  144. * {@inheritdoc}
  145. *
  146. * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
  147. */
  148. protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
  149. {
  150. $indexBuffer = [];
  151. // fetch primary
  152. $indexArray = $this->_conn->fetchAllAssociative(sprintf(
  153. 'PRAGMA TABLE_INFO (%s)',
  154. $this->_conn->quote($tableName)
  155. ));
  156. usort(
  157. $indexArray,
  158. /**
  159. * @param array<string,mixed> $a
  160. * @param array<string,mixed> $b
  161. */
  162. static function (array $a, array $b): int {
  163. if ($a['pk'] === $b['pk']) {
  164. return $a['cid'] - $b['cid'];
  165. }
  166. return $a['pk'] - $b['pk'];
  167. }
  168. );
  169. foreach ($indexArray as $indexColumnRow) {
  170. if ($indexColumnRow['pk'] === '0') {
  171. continue;
  172. }
  173. $indexBuffer[] = [
  174. 'key_name' => 'primary',
  175. 'primary' => true,
  176. 'non_unique' => false,
  177. 'column_name' => $indexColumnRow['name'],
  178. ];
  179. }
  180. // fetch regular indexes
  181. foreach ($tableIndexes as $tableIndex) {
  182. // Ignore indexes with reserved names, e.g. autoindexes
  183. if (strpos($tableIndex['name'], 'sqlite_') === 0) {
  184. continue;
  185. }
  186. $keyName = $tableIndex['name'];
  187. $idx = [];
  188. $idx['key_name'] = $keyName;
  189. $idx['primary'] = false;
  190. $idx['non_unique'] = ! $tableIndex['unique'];
  191. $indexArray = $this->_conn->fetchAllAssociative(sprintf(
  192. 'PRAGMA INDEX_INFO (%s)',
  193. $this->_conn->quote($keyName)
  194. ));
  195. foreach ($indexArray as $indexColumnRow) {
  196. $idx['column_name'] = $indexColumnRow['name'];
  197. $indexBuffer[] = $idx;
  198. }
  199. }
  200. return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
  201. }
  202. /**
  203. * @deprecated
  204. *
  205. * @param array<string, mixed> $tableIndex
  206. *
  207. * @return array<string, bool|string>
  208. */
  209. protected function _getPortableTableIndexDefinition($tableIndex)
  210. {
  211. return [
  212. 'name' => $tableIndex['name'],
  213. 'unique' => (bool) $tableIndex['unique'],
  214. ];
  215. }
  216. /**
  217. * {@inheritdoc}
  218. */
  219. protected function _getPortableTableColumnList($table, $database, $tableColumns)
  220. {
  221. $list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
  222. // find column with autoincrement
  223. $autoincrementColumn = null;
  224. $autoincrementCount = 0;
  225. foreach ($tableColumns as $tableColumn) {
  226. if ($tableColumn['pk'] === '0') {
  227. continue;
  228. }
  229. $autoincrementCount++;
  230. if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') {
  231. continue;
  232. }
  233. $autoincrementColumn = $tableColumn['name'];
  234. }
  235. if ($autoincrementCount === 1 && $autoincrementColumn !== null) {
  236. foreach ($list as $column) {
  237. if ($autoincrementColumn !== $column->getName()) {
  238. continue;
  239. }
  240. $column->setAutoincrement(true);
  241. }
  242. }
  243. // inspect column collation and comments
  244. $createSql = $this->getCreateTableSQL($table) ?? '';
  245. foreach ($list as $columnName => $column) {
  246. $type = $column->getType();
  247. if ($type instanceof StringType || $type instanceof TextType) {
  248. $column->setPlatformOption(
  249. 'collation',
  250. $this->parseColumnCollationFromSQL($columnName, $createSql) ?: 'BINARY'
  251. );
  252. }
  253. $comment = $this->parseColumnCommentFromSQL($columnName, $createSql);
  254. if ($comment === null) {
  255. continue;
  256. }
  257. $type = $this->extractDoctrineTypeFromComment($comment, '');
  258. if ($type !== '') {
  259. $column->setType(Type::getType($type));
  260. $comment = $this->removeDoctrineTypeFromComment($comment, $type);
  261. }
  262. $column->setComment($comment);
  263. }
  264. return $list;
  265. }
  266. /**
  267. * {@inheritdoc}
  268. */
  269. protected function _getPortableTableColumnDefinition($tableColumn)
  270. {
  271. $parts = explode('(', $tableColumn['type']);
  272. $tableColumn['type'] = trim($parts[0]);
  273. if (isset($parts[1])) {
  274. $length = trim($parts[1], ')');
  275. $tableColumn['length'] = $length;
  276. }
  277. $dbType = strtolower($tableColumn['type']);
  278. $length = $tableColumn['length'] ?? null;
  279. $unsigned = false;
  280. if (strpos($dbType, ' unsigned') !== false) {
  281. $dbType = str_replace(' unsigned', '', $dbType);
  282. $unsigned = true;
  283. }
  284. $fixed = false;
  285. $type = $this->_platform->getDoctrineTypeMapping($dbType);
  286. $default = $tableColumn['dflt_value'];
  287. if ($default === 'NULL') {
  288. $default = null;
  289. }
  290. if ($default !== null) {
  291. // SQLite returns the default value as a literal expression, so we need to parse it
  292. if (preg_match('/^\'(.*)\'$/s', $default, $matches)) {
  293. $default = str_replace("''", "'", $matches[1]);
  294. }
  295. }
  296. $notnull = (bool) $tableColumn['notnull'];
  297. if (! isset($tableColumn['name'])) {
  298. $tableColumn['name'] = '';
  299. }
  300. $precision = null;
  301. $scale = null;
  302. switch ($dbType) {
  303. case 'char':
  304. $fixed = true;
  305. break;
  306. case 'float':
  307. case 'double':
  308. case 'real':
  309. case 'decimal':
  310. case 'numeric':
  311. if (isset($tableColumn['length'])) {
  312. if (strpos($tableColumn['length'], ',') === false) {
  313. $tableColumn['length'] .= ',0';
  314. }
  315. [$precision, $scale] = array_map('trim', explode(',', $tableColumn['length']));
  316. }
  317. $length = null;
  318. break;
  319. }
  320. $options = [
  321. 'length' => $length,
  322. 'unsigned' => $unsigned,
  323. 'fixed' => $fixed,
  324. 'notnull' => $notnull,
  325. 'default' => $default,
  326. 'precision' => $precision,
  327. 'scale' => $scale,
  328. 'autoincrement' => false,
  329. ];
  330. return new Column($tableColumn['name'], Type::getType($type), $options);
  331. }
  332. /**
  333. * {@inheritdoc}
  334. */
  335. protected function _getPortableViewDefinition($view)
  336. {
  337. return new View($view['name'], $view['sql']);
  338. }
  339. /**
  340. * {@inheritdoc}
  341. */
  342. protected function _getPortableTableForeignKeysList($tableForeignKeys)
  343. {
  344. $list = [];
  345. foreach ($tableForeignKeys as $value) {
  346. $value = array_change_key_case($value, CASE_LOWER);
  347. $name = $value['constraint_name'];
  348. if (! isset($list[$name])) {
  349. if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') {
  350. $value['on_delete'] = null;
  351. }
  352. if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') {
  353. $value['on_update'] = null;
  354. }
  355. $list[$name] = [
  356. 'name' => $name,
  357. 'local' => [],
  358. 'foreign' => [],
  359. 'foreignTable' => $value['table'],
  360. 'onDelete' => $value['on_delete'],
  361. 'onUpdate' => $value['on_update'],
  362. 'deferrable' => $value['deferrable'],
  363. 'deferred' => $value['deferred'],
  364. ];
  365. }
  366. $list[$name]['local'][] = $value['from'];
  367. $list[$name]['foreign'][] = $value['to'];
  368. }
  369. $result = [];
  370. foreach ($list as $constraint) {
  371. $result[] = new ForeignKeyConstraint(
  372. array_values($constraint['local']),
  373. $constraint['foreignTable'],
  374. array_values($constraint['foreign']),
  375. $constraint['name'],
  376. [
  377. 'onDelete' => $constraint['onDelete'],
  378. 'onUpdate' => $constraint['onUpdate'],
  379. 'deferrable' => $constraint['deferrable'],
  380. 'deferred' => $constraint['deferred'],
  381. ]
  382. );
  383. }
  384. return $result;
  385. }
  386. /**
  387. * @param Table|string $table
  388. *
  389. * @return TableDiff
  390. *
  391. * @throws Exception
  392. */
  393. private function getTableDiffForAlterForeignKey($table)
  394. {
  395. if (! $table instanceof Table) {
  396. $tableDetails = $this->tryMethod('listTableDetails', $table);
  397. if ($tableDetails === false) {
  398. throw new Exception(
  399. sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table)
  400. );
  401. }
  402. $table = $tableDetails;
  403. }
  404. $tableDiff = new TableDiff($table->getName());
  405. $tableDiff->fromTable = $table;
  406. return $tableDiff;
  407. }
  408. private function parseColumnCollationFromSQL(string $column, string $sql): ?string
  409. {
  410. $pattern = '{(?:\W' . preg_quote($column) . '\W|\W'
  411. . preg_quote($this->_platform->quoteSingleIdentifier($column))
  412. . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';
  413. if (preg_match($pattern, $sql, $match) !== 1) {
  414. return null;
  415. }
  416. return $match[1];
  417. }
  418. private function parseTableCommentFromSQL(string $table, string $sql): ?string
  419. {
  420. $pattern = '/\s* # Allow whitespace characters at start of line
  421. CREATE\sTABLE # Match "CREATE TABLE"
  422. (?:\W"' . preg_quote($this->_platform->quoteSingleIdentifier($table), '/') . '"\W|\W' . preg_quote($table, '/')
  423. . '\W) # Match table name (quoted and unquoted)
  424. ( # Start capture
  425. (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s)
  426. )/ix';
  427. if (preg_match($pattern, $sql, $match) !== 1) {
  428. return null;
  429. }
  430. $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
  431. return $comment === '' ? null : $comment;
  432. }
  433. private function parseColumnCommentFromSQL(string $column, string $sql): ?string
  434. {
  435. $pattern = '{[\s(,](?:\W' . preg_quote($this->_platform->quoteSingleIdentifier($column))
  436. . '\W|\W' . preg_quote($column) . '\W)(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';
  437. if (preg_match($pattern, $sql, $match) !== 1) {
  438. return null;
  439. }
  440. $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
  441. return $comment === '' ? null : $comment;
  442. }
  443. private function getCreateTableSQL(string $table): ?string
  444. {
  445. return $this->_conn->fetchColumn(
  446. <<<'SQL'
  447. SELECT sql
  448. FROM (
  449. SELECT *
  450. FROM sqlite_master
  451. UNION ALL
  452. SELECT *
  453. FROM sqlite_temp_master
  454. )
  455. WHERE type = 'table'
  456. AND name = ?
  457. SQL
  458. ,
  459. [$table]
  460. ) ?: null;
  461. }
  462. /**
  463. * @param string $name
  464. */
  465. public function listTableDetails($name): Table
  466. {
  467. $table = parent::listTableDetails($name);
  468. $tableCreateSql = $this->getCreateTableSQL($name) ?? '';
  469. $comment = $this->parseTableCommentFromSQL($name, $tableCreateSql);
  470. if ($comment !== null) {
  471. $table->addOption('comment', $comment);
  472. }
  473. return $table;
  474. }
  475. }