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