summaryrefslogtreecommitdiff
path: root/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php')
-rw-r--r--vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php620
1 files changed, 620 insertions, 0 deletions
diff --git a/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php b/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php
new file mode 100644
index 0000000..c001c25
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php
@@ -0,0 +1,620 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Exception;
8use Doctrine\DBAL\Platforms\SQLite;
9use Doctrine\DBAL\Platforms\SQLitePlatform;
10use Doctrine\DBAL\Result;
11use Doctrine\DBAL\Types\StringType;
12use Doctrine\DBAL\Types\TextType;
13use Doctrine\DBAL\Types\Type;
14
15use function array_change_key_case;
16use function array_merge;
17use function assert;
18use function count;
19use function implode;
20use function is_string;
21use function preg_match;
22use function preg_match_all;
23use function preg_quote;
24use function preg_replace;
25use function rtrim;
26use function str_contains;
27use function str_replace;
28use function str_starts_with;
29use function strcasecmp;
30use function strtolower;
31use function trim;
32use function usort;
33
34use const CASE_LOWER;
35
36/**
37 * SQLite SchemaManager.
38 *
39 * @extends AbstractSchemaManager<SQLitePlatform>
40 */
41class SQLiteSchemaManager extends AbstractSchemaManager
42{
43 /**
44 * {@inheritDoc}
45 */
46 protected function fetchForeignKeyColumnsByTable(string $databaseName): array
47 {
48 $columnsByTable = parent::fetchForeignKeyColumnsByTable($databaseName);
49
50 if (count($columnsByTable) > 0) {
51 foreach ($columnsByTable as $table => $columns) {
52 $columnsByTable[$table] = $this->addDetailsToTableForeignKeyColumns($table, $columns);
53 }
54 }
55
56 return $columnsByTable;
57 }
58
59 public function createForeignKey(ForeignKeyConstraint $foreignKey, string $table): void
60 {
61 $table = $this->introspectTable($table);
62
63 $this->alterTable(new TableDiff($table, [], [], [], [], [], [], [], [], [$foreignKey], [], []));
64 }
65
66 public function dropForeignKey(string $name, string $table): void
67 {
68 $table = $this->introspectTable($table);
69
70 $foreignKey = $table->getForeignKey($name);
71
72 $this->alterTable(new TableDiff($table, [], [], [], [], [], [], [], [], [], [], [$foreignKey]));
73 }
74
75 /**
76 * {@inheritDoc}
77 */
78 public function listTableForeignKeys(string $table): array
79 {
80 $table = $this->normalizeName($table);
81
82 $columns = $this->selectForeignKeyColumns('main', $table)
83 ->fetchAllAssociative();
84
85 if (count($columns) > 0) {
86 $columns = $this->addDetailsToTableForeignKeyColumns($table, $columns);
87 }
88
89 return $this->_getPortableTableForeignKeysList($columns);
90 }
91
92 /**
93 * {@inheritDoc}
94 */
95 protected function _getPortableTableDefinition(array $table): string
96 {
97 return $table['table_name'];
98 }
99
100 /**
101 * {@inheritDoc}
102 *
103 * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
104 */
105 protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array
106 {
107 $indexBuffer = [];
108
109 // fetch primary
110 $indexArray = $this->connection->fetchAllAssociative('SELECT * FROM PRAGMA_TABLE_INFO (?)', [$tableName]);
111
112 usort(
113 $indexArray,
114 /**
115 * @param array<string,mixed> $a
116 * @param array<string,mixed> $b
117 */
118 static function (array $a, array $b): int {
119 if ($a['pk'] === $b['pk']) {
120 return $a['cid'] - $b['cid'];
121 }
122
123 return $a['pk'] - $b['pk'];
124 },
125 );
126
127 foreach ($indexArray as $indexColumnRow) {
128 if ($indexColumnRow['pk'] === 0 || $indexColumnRow['pk'] === '0') {
129 continue;
130 }
131
132 $indexBuffer[] = [
133 'key_name' => 'primary',
134 'primary' => true,
135 'non_unique' => false,
136 'column_name' => $indexColumnRow['name'],
137 ];
138 }
139
140 // fetch regular indexes
141 foreach ($tableIndexes as $tableIndex) {
142 // Ignore indexes with reserved names, e.g. autoindexes
143 if (str_starts_with($tableIndex['name'], 'sqlite_')) {
144 continue;
145 }
146
147 $keyName = $tableIndex['name'];
148 $idx = [];
149 $idx['key_name'] = $keyName;
150 $idx['primary'] = false;
151 $idx['non_unique'] = ! $tableIndex['unique'];
152
153 $indexArray = $this->connection->fetchAllAssociative('SELECT * FROM PRAGMA_INDEX_INFO (?)', [$keyName]);
154
155 foreach ($indexArray as $indexColumnRow) {
156 $idx['column_name'] = $indexColumnRow['name'];
157 $indexBuffer[] = $idx;
158 }
159 }
160
161 return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
162 }
163
164 /**
165 * {@inheritDoc}
166 */
167 protected function _getPortableTableColumnList(string $table, string $database, array $tableColumns): array
168 {
169 $list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
170
171 // find column with autoincrement
172 $autoincrementColumn = null;
173 $autoincrementCount = 0;
174
175 foreach ($tableColumns as $tableColumn) {
176 if ($tableColumn['pk'] === 0 || $tableColumn['pk'] === '0') {
177 continue;
178 }
179
180 $autoincrementCount++;
181 if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') {
182 continue;
183 }
184
185 $autoincrementColumn = $tableColumn['name'];
186 }
187
188 if ($autoincrementCount === 1 && $autoincrementColumn !== null) {
189 foreach ($list as $column) {
190 if ($autoincrementColumn !== $column->getName()) {
191 continue;
192 }
193
194 $column->setAutoincrement(true);
195 }
196 }
197
198 // inspect column collation and comments
199 $createSql = $this->getCreateTableSQL($table);
200
201 foreach ($list as $columnName => $column) {
202 $type = $column->getType();
203
204 if ($type instanceof StringType || $type instanceof TextType) {
205 $column->setPlatformOption(
206 'collation',
207 $this->parseColumnCollationFromSQL($columnName, $createSql) ?? 'BINARY',
208 );
209 }
210
211 $comment = $this->parseColumnCommentFromSQL($columnName, $createSql);
212
213 $column->setComment($comment);
214 }
215
216 return $list;
217 }
218
219 /**
220 * {@inheritDoc}
221 */
222 protected function _getPortableTableColumnDefinition(array $tableColumn): Column
223 {
224 preg_match('/^([^()]*)\\s*(\\(((\\d+)(,\\s*(\\d+))?)\\))?/', $tableColumn['type'], $matches);
225
226 $dbType = trim(strtolower($matches[1]));
227
228 $length = $precision = $unsigned = null;
229 $fixed = $unsigned = false;
230 $scale = 0;
231
232 if (count($matches) >= 6) {
233 $precision = (int) $matches[4];
234 $scale = (int) $matches[6];
235 } elseif (count($matches) >= 4) {
236 $length = (int) $matches[4];
237 }
238
239 if (str_contains($dbType, ' unsigned')) {
240 $dbType = str_replace(' unsigned', '', $dbType);
241 $unsigned = true;
242 }
243
244 $type = $this->platform->getDoctrineTypeMapping($dbType);
245 $default = $tableColumn['dflt_value'];
246 if ($default === 'NULL') {
247 $default = null;
248 }
249
250 if ($default !== null) {
251 // SQLite returns the default value as a literal expression, so we need to parse it
252 if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) {
253 $default = str_replace("''", "'", $matches[1]);
254 }
255 }
256
257 $notnull = (bool) $tableColumn['notnull'];
258
259 if (! isset($tableColumn['name'])) {
260 $tableColumn['name'] = '';
261 }
262
263 if ($dbType === 'char') {
264 $fixed = true;
265 }
266
267 $options = [
268 'length' => $length,
269 'unsigned' => $unsigned,
270 'fixed' => $fixed,
271 'notnull' => $notnull,
272 'default' => $default,
273 'precision' => $precision,
274 'scale' => $scale,
275 ];
276
277 return new Column($tableColumn['name'], Type::getType($type), $options);
278 }
279
280 /**
281 * {@inheritDoc}
282 */
283 protected function _getPortableViewDefinition(array $view): View
284 {
285 return new View($view['name'], $view['sql']);
286 }
287
288 /**
289 * {@inheritDoc}
290 */
291 protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array
292 {
293 $list = [];
294 foreach ($tableForeignKeys as $value) {
295 $value = array_change_key_case($value, CASE_LOWER);
296 $id = $value['id'];
297 if (! isset($list[$id])) {
298 if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') {
299 $value['on_delete'] = null;
300 }
301
302 if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') {
303 $value['on_update'] = null;
304 }
305
306 $list[$id] = [
307 'name' => $value['constraint_name'],
308 'local' => [],
309 'foreign' => [],
310 'foreignTable' => $value['table'],
311 'onDelete' => $value['on_delete'],
312 'onUpdate' => $value['on_update'],
313 'deferrable' => $value['deferrable'],
314 'deferred' => $value['deferred'],
315 ];
316 }
317
318 $list[$id]['local'][] = $value['from'];
319
320 if ($value['to'] === null) {
321 // Inferring a shorthand form for the foreign key constraint, where the "to" field is empty.
322 // @see https://www.sqlite.org/foreignkeys.html#fk_indexes.
323 $foreignTableIndexes = $this->_getPortableTableIndexesList([], $value['table']);
324
325 if (! isset($foreignTableIndexes['primary'])) {
326 continue;
327 }
328
329 $list[$id]['foreign'] = [...$list[$id]['foreign'], ...$foreignTableIndexes['primary']->getColumns()];
330
331 continue;
332 }
333
334 $list[$id]['foreign'][] = $value['to'];
335 }
336
337 return parent::_getPortableTableForeignKeysList($list);
338 }
339
340 /**
341 * {@inheritDoc}
342 */
343 protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
344 {
345 return new ForeignKeyConstraint(
346 $tableForeignKey['local'],
347 $tableForeignKey['foreignTable'],
348 $tableForeignKey['foreign'],
349 $tableForeignKey['name'],
350 [
351 'onDelete' => $tableForeignKey['onDelete'],
352 'onUpdate' => $tableForeignKey['onUpdate'],
353 'deferrable' => $tableForeignKey['deferrable'],
354 'deferred' => $tableForeignKey['deferred'],
355 ],
356 );
357 }
358
359 private function parseColumnCollationFromSQL(string $column, string $sql): ?string
360 {
361 $pattern = '{(?:\W' . preg_quote($column) . '\W|\W'
362 . preg_quote($this->platform->quoteSingleIdentifier($column))
363 . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';
364
365 if (preg_match($pattern, $sql, $match) !== 1) {
366 return null;
367 }
368
369 return $match[1];
370 }
371
372 private function parseTableCommentFromSQL(string $table, string $sql): ?string
373 {
374 $pattern = '/\s* # Allow whitespace characters at start of line
375CREATE\sTABLE # Match "CREATE TABLE"
376(?:\W"' . preg_quote($this->platform->quoteSingleIdentifier($table), '/') . '"\W|\W' . preg_quote($table, '/')
377 . '\W) # Match table name (quoted and unquoted)
378( # Start capture
379 (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s)
380)/ix';
381
382 if (preg_match($pattern, $sql, $match) !== 1) {
383 return null;
384 }
385
386 $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
387
388 return $comment === '' ? null : $comment;
389 }
390
391 private function parseColumnCommentFromSQL(string $column, string $sql): string
392 {
393 $pattern = '{[\s(,](?:\W' . preg_quote($this->platform->quoteSingleIdentifier($column))
394 . '\W|\W' . preg_quote($column) . '\W)(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';
395
396 if (preg_match($pattern, $sql, $match) !== 1) {
397 return '';
398 }
399
400 $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
401 assert(is_string($comment));
402
403 return $comment;
404 }
405
406 /** @throws Exception */
407 private function getCreateTableSQL(string $table): string
408 {
409 $sql = $this->connection->fetchOne(
410 <<<'SQL'
411SELECT sql
412 FROM (
413 SELECT *
414 FROM sqlite_master
415 UNION ALL
416 SELECT *
417 FROM sqlite_temp_master
418 )
419WHERE type = 'table'
420AND name = ?
421SQL
422 ,
423 [$table],
424 );
425
426 if ($sql !== false) {
427 return $sql;
428 }
429
430 return '';
431 }
432
433 /**
434 * @param list<array<string,mixed>> $columns
435 *
436 * @return list<array<string,mixed>>
437 *
438 * @throws Exception
439 */
440 private function addDetailsToTableForeignKeyColumns(string $table, array $columns): array
441 {
442 $foreignKeyDetails = $this->getForeignKeyDetails($table);
443 $foreignKeyCount = count($foreignKeyDetails);
444
445 foreach ($columns as $i => $column) {
446 // SQLite identifies foreign keys in reverse order of appearance in SQL
447 $columns[$i] = array_merge($column, $foreignKeyDetails[$foreignKeyCount - $column['id'] - 1]);
448 }
449
450 return $columns;
451 }
452
453 /**
454 * @return list<array<string, mixed>>
455 *
456 * @throws Exception
457 */
458 private function getForeignKeyDetails(string $table): array
459 {
460 $createSql = $this->getCreateTableSQL($table);
461
462 if (
463 preg_match_all(
464 '#
465 (?:CONSTRAINT\s+(\S+)\s+)?
466 (?:FOREIGN\s+KEY[^)]+\)\s*)?
467 REFERENCES\s+\S+\s*(?:\([^)]+\))?
468 (?:
469 [^,]*?
470 (NOT\s+DEFERRABLE|DEFERRABLE)
471 (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
472 )?#isx',
473 $createSql,
474 $match,
475 ) === 0
476 ) {
477 return [];
478 }
479
480 $names = $match[1];
481 $deferrable = $match[2];
482 $deferred = $match[3];
483 $details = [];
484
485 for ($i = 0, $count = count($match[0]); $i < $count; $i++) {
486 $details[] = [
487 'constraint_name' => $names[$i] ?? '',
488 'deferrable' => isset($deferrable[$i]) && strcasecmp($deferrable[$i], 'deferrable') === 0,
489 'deferred' => isset($deferred[$i]) && strcasecmp($deferred[$i], 'deferred') === 0,
490 ];
491 }
492
493 return $details;
494 }
495
496 public function createComparator(): Comparator
497 {
498 return new SQLite\Comparator($this->platform);
499 }
500
501 protected function selectTableNames(string $databaseName): Result
502 {
503 $sql = <<<'SQL'
504SELECT name AS table_name
505FROM sqlite_master
506WHERE type = 'table'
507 AND name != 'sqlite_sequence'
508 AND name != 'geometry_columns'
509 AND name != 'spatial_ref_sys'
510UNION ALL
511SELECT name
512FROM sqlite_temp_master
513WHERE type = 'table'
514ORDER BY name
515SQL;
516
517 return $this->connection->executeQuery($sql);
518 }
519
520 protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
521 {
522 $sql = <<<'SQL'
523 SELECT t.name AS table_name,
524 c.*
525 FROM sqlite_master t
526 JOIN pragma_table_info(t.name) c
527SQL;
528
529 $conditions = [
530 "t.type = 'table'",
531 "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')",
532 ];
533 $params = [];
534
535 if ($tableName !== null) {
536 $conditions[] = 't.name = ?';
537 $params[] = str_replace('.', '__', $tableName);
538 }
539
540 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, c.cid';
541
542 return $this->connection->executeQuery($sql, $params);
543 }
544
545 protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
546 {
547 $sql = <<<'SQL'
548 SELECT t.name AS table_name,
549 i.*
550 FROM sqlite_master t
551 JOIN pragma_index_list(t.name) i
552SQL;
553
554 $conditions = [
555 "t.type = 'table'",
556 "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')",
557 ];
558 $params = [];
559
560 if ($tableName !== null) {
561 $conditions[] = 't.name = ?';
562 $params[] = str_replace('.', '__', $tableName);
563 }
564
565 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, i.seq';
566
567 return $this->connection->executeQuery($sql, $params);
568 }
569
570 protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
571 {
572 $sql = <<<'SQL'
573 SELECT t.name AS table_name,
574 p.*
575 FROM sqlite_master t
576 JOIN pragma_foreign_key_list(t.name) p
577 ON p."seq" != '-1'
578SQL;
579
580 $conditions = [
581 "t.type = 'table'",
582 "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')",
583 ];
584 $params = [];
585
586 if ($tableName !== null) {
587 $conditions[] = 't.name = ?';
588 $params[] = str_replace('.', '__', $tableName);
589 }
590
591 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, p.id DESC, p.seq';
592
593 return $this->connection->executeQuery($sql, $params);
594 }
595
596 /**
597 * {@inheritDoc}
598 */
599 protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
600 {
601 if ($tableName === null) {
602 $tables = $this->listTableNames();
603 } else {
604 $tables = [$tableName];
605 }
606
607 $tableOptions = [];
608 foreach ($tables as $table) {
609 $comment = $this->parseTableCommentFromSQL($table, $this->getCreateTableSQL($table));
610
611 if ($comment === null) {
612 continue;
613 }
614
615 $tableOptions[$table]['comment'] = $comment;
616 }
617
618 return $tableOptions;
619 }
620}