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