vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php line 288

Open in your IDE?
  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'] === || $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'] === || $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 === && $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($valueCASE_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.             if ($value['to'] === null) {
  368.                 continue;
  369.             }
  370.             $list[$name]['foreign'][] = $value['to'];
  371.         }
  372.         $result = [];
  373.         foreach ($list as $constraint) {
  374.             $result[] = new ForeignKeyConstraint(
  375.                 array_values($constraint['local']),
  376.                 $constraint['foreignTable'],
  377.                 array_values($constraint['foreign']),
  378.                 $constraint['name'],
  379.                 [
  380.                     'onDelete' => $constraint['onDelete'],
  381.                     'onUpdate' => $constraint['onUpdate'],
  382.                     'deferrable' => $constraint['deferrable'],
  383.                     'deferred' => $constraint['deferred'],
  384.                 ]
  385.             );
  386.         }
  387.         return $result;
  388.     }
  389.     /**
  390.      * @param Table|string $table
  391.      *
  392.      * @return TableDiff
  393.      *
  394.      * @throws Exception
  395.      */
  396.     private function getTableDiffForAlterForeignKey($table)
  397.     {
  398.         if (! $table instanceof Table) {
  399.             $tableDetails $this->tryMethod('listTableDetails'$table);
  400.             if ($tableDetails === false) {
  401.                 throw new Exception(
  402.                     sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".'$table)
  403.                 );
  404.             }
  405.             $table $tableDetails;
  406.         }
  407.         $tableDiff            = new TableDiff($table->getName());
  408.         $tableDiff->fromTable $table;
  409.         return $tableDiff;
  410.     }
  411.     private function parseColumnCollationFromSQL(string $columnstring $sql): ?string
  412.     {
  413.         $pattern '{(?:\W' preg_quote($column) . '\W|\W'
  414.             preg_quote($this->_platform->quoteSingleIdentifier($column))
  415.             . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';
  416.         if (preg_match($pattern$sql$match) !== 1) {
  417.             return null;
  418.         }
  419.         return $match[1];
  420.     }
  421.     private function parseTableCommentFromSQL(string $tablestring $sql): ?string
  422.     {
  423.         $pattern '/\s* # Allow whitespace characters at start of line
  424. CREATE\sTABLE # Match "CREATE TABLE"
  425. (?:\W"' preg_quote($this->_platform->quoteSingleIdentifier($table), '/') . '"\W|\W' preg_quote($table'/')
  426.             . '\W) # Match table name (quoted and unquoted)
  427. ( # Start capture
  428.    (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s)
  429. )/ix';
  430.         if (preg_match($pattern$sql$match) !== 1) {
  431.             return null;
  432.         }
  433.         $comment preg_replace('{^\s*--}m'''rtrim($match[1], "\n"));
  434.         return $comment === '' null $comment;
  435.     }
  436.     private function parseColumnCommentFromSQL(string $columnstring $sql): ?string
  437.     {
  438.         $pattern '{[\s(,](?:\W' preg_quote($this->_platform->quoteSingleIdentifier($column))
  439.             . '\W|\W' preg_quote($column) . '\W)(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';
  440.         if (preg_match($pattern$sql$match) !== 1) {
  441.             return null;
  442.         }
  443.         $comment preg_replace('{^\s*--}m'''rtrim($match[1], "\n"));
  444.         return $comment === '' null $comment;
  445.     }
  446.     private function getCreateTableSQL(string $table): ?string
  447.     {
  448.         return $this->_conn->fetchColumn(
  449.             <<<'SQL'
  450. SELECT sql
  451.   FROM (
  452.       SELECT *
  453.         FROM sqlite_master
  454.    UNION ALL
  455.       SELECT *
  456.         FROM sqlite_temp_master
  457.   )
  458. WHERE type = 'table'
  459. AND name = ?
  460. SQL
  461.             ,
  462.             [$table]
  463.         ) ?: null;
  464.     }
  465.     /**
  466.      * @param string $name
  467.      */
  468.     public function listTableDetails($name): Table
  469.     {
  470.         $table parent::listTableDetails($name);
  471.         $tableCreateSql $this->getCreateTableSQL($name) ?? '';
  472.         $comment $this->parseTableCommentFromSQL($name$tableCreateSql);
  473.         if ($comment !== null) {
  474.             $table->addOption('comment'$comment);
  475.         }
  476.         return $table;
  477.     }
  478. }