SQLServerSchemaManager.php 10.7 KB
Newer Older
romanb's avatar
romanb committed
1 2
<?php

Michael Moravec's avatar
Michael Moravec committed
3 4
declare(strict_types=1);

5
namespace Doctrine\DBAL\Schema;
romanb's avatar
romanb committed
6

7 8
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Driver\DriverException;
9
use Doctrine\DBAL\Platforms\SQLServerPlatform;
10
use Doctrine\DBAL\Types\Type;
11
use PDOException;
Sergei Morozov's avatar
Sergei Morozov committed
12
use Throwable;
Sergei Morozov's avatar
Sergei Morozov committed
13
use function assert;
14 15
use function count;
use function in_array;
Sergei Morozov's avatar
Sergei Morozov committed
16 17
use function is_string;
use function preg_match;
18 19 20 21
use function sprintf;
use function str_replace;
use function strpos;
use function strtok;
22

romanb's avatar
romanb committed
23
/**
Benjamin Morel's avatar
Benjamin Morel committed
24
 * SQL Server Schema Manager.
romanb's avatar
romanb committed
25
 */
26
class SQLServerSchemaManager extends AbstractSchemaManager
27
{
28 29 30
    /**
     * {@inheritdoc}
     */
31
    public function dropDatabase(string $database) : void
32 33 34 35 36
    {
        try {
            parent::dropDatabase($database);
        } catch (DBALException $exception) {
            $exception = $exception->getPrevious();
Sergei Morozov's avatar
Sergei Morozov committed
37
            assert($exception instanceof Throwable);
38 39 40 41 42 43 44 45 46

            if (! $exception instanceof DriverException) {
                throw $exception;
            }

            // If we have a error code 3702, the drop database operation failed
            // because of active connections on the database.
            // To force dropping the database, we first have to close all active connections
            // on that database and issue the drop database operation again.
47
            if ($exception->getCode() !== 3702) {
48 49 50 51 52 53 54 55 56
                throw $exception;
            }

            $this->closeActiveDatabaseConnections($database);

            parent::dropDatabase($database);
        }
    }

57 58 59
    /**
     * {@inheritdoc}
     */
60
    protected function _getPortableSequenceDefinition(array $sequence) : Sequence
61
    {
62
        return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']);
63 64
    }

romanb's avatar
romanb committed
65
    /**
66
     * {@inheritdoc}
romanb's avatar
romanb committed
67
     */
68
    protected function _getPortableTableColumnDefinition(array $tableColumn) : Column
romanb's avatar
romanb committed
69
    {
Sergei Morozov's avatar
Sergei Morozov committed
70 71 72
        $dbType = strtok($tableColumn['type'], '(), ');
        assert(is_string($dbType));

73 74 75 76 77 78
        $length = (int) $tableColumn['length'];

        $precision = $default = null;

        $scale = 0;
        $fixed = false;
romanb's avatar
romanb committed
79

80
        if (! isset($tableColumn['name'])) {
81
            $tableColumn['name'] = '';
romanb's avatar
romanb committed
82
        }
83

84 85 86 87 88 89 90 91 92 93
        if ($tableColumn['scale'] !== null) {
            $scale = (int) $tableColumn['scale'];
        }

        if ($tableColumn['precision'] !== null) {
            $precision = (int) $tableColumn['precision'];
        }

        if ($tableColumn['default'] !== null) {
            $default = $this->parseDefaultExpression($tableColumn['default']);
94
        }
95

96 97 98 99 100
        switch ($dbType) {
            case 'nchar':
            case 'nvarchar':
            case 'ntext':
                // Unicode data requires 2 bytes per character
101
                $length /= 2;
102 103 104
                break;
            case 'varchar':
                // TEXT type is returned as VARCHAR(MAX) with a length of -1
105
                if ($length === -1) {
106 107 108 109
                    $dbType = 'text';
                }
                break;
        }
110

111
        if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') {
Steve Müller's avatar
Steve Müller committed
112 113 114
            $fixed = true;
        }

115 116
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'])
            ?? $this->_platform->getDoctrineTypeMapping($dbType);
117

118
        $options = [
119
            'length'        => $length === 0 || ! in_array($type, ['text', 'string']) ? null : $length,
120
            'fixed'         => $fixed,
121
            'default'       => $default,
122
            'notnull'       => (bool) $tableColumn['notnull'],
123 124
            'scale'         => $scale,
            'precision'     => $precision,
125
            'autoincrement' => (bool) $tableColumn['autoincrement'],
126
            'comment'       => $tableColumn['comment'] !== '' ? $tableColumn['comment'] : null,
127
        ];
128

129
        $column = new Column($tableColumn['name'], Type::getType($type), $options);
130 131 132 133

        if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') {
            $column->setPlatformOption('collation', $tableColumn['collation']);
        }
134 135

        return $column;
romanb's avatar
romanb committed
136 137
    }

138
    private function parseDefaultExpression(string $value) : ?string
Sergei Morozov's avatar
Sergei Morozov committed
139
    {
140 141 142 143 144 145 146 147 148 149
        while (preg_match('/^\((.*)\)$/s', $value, $matches)) {
            $value = $matches[1];
        }

        if ($value === 'NULL') {
            return null;
        }

        if (preg_match('/^\'(.*)\'$/s', $value, $matches)) {
            $value = str_replace("''", "'", $matches[1]);
Sergei Morozov's avatar
Sergei Morozov committed
150 151 152 153 154 155 156 157 158
        }

        if ($value === 'getdate()') {
            return $this->_platform->getCurrentTimestampSQL();
        }

        return $value;
    }

159 160 161
    /**
     * {@inheritdoc}
     */
162
    protected function _getPortableTableForeignKeysList(array $tableForeignKeys) : array
163
    {
164
        $foreignKeys = [];
165 166

        foreach ($tableForeignKeys as $tableForeignKey) {
167
            if (! isset($foreignKeys[$tableForeignKey['ForeignKey']])) {
168 169
                $foreignKeys[$tableForeignKey['ForeignKey']] = [
                    'local_columns' => [$tableForeignKey['ColumnName']],
170
                    'foreign_table' => $tableForeignKey['ReferenceTableName'],
171
                    'foreign_columns' => [$tableForeignKey['ReferenceColumnName']],
172
                    'name' => $tableForeignKey['ForeignKey'],
173
                    'options' => [
174
                        'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']),
175 176
                        'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']),
                    ],
177
                ];
178
            } else {
179
                $foreignKeys[$tableForeignKey['ForeignKey']]['local_columns'][]   = $tableForeignKey['ColumnName'];
180 181 182 183 184 185 186
                $foreignKeys[$tableForeignKey['ForeignKey']]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName'];
            }
        }

        return parent::_getPortableTableForeignKeysList($foreignKeys);
    }

romanb's avatar
romanb committed
187
    /**
188
     * {@inheritdoc}
romanb's avatar
romanb committed
189
     */
190
    protected function _getPortableTableIndexesList(array $tableIndexRows, string $tableName) : array
romanb's avatar
romanb committed
191
    {
192
        foreach ($tableIndexRows as &$tableIndex) {
193 194 195
            $tableIndex['non_unique'] = (bool) $tableIndex['non_unique'];
            $tableIndex['primary']    = (bool) $tableIndex['primary'];
            $tableIndex['flags']      = $tableIndex['flags'] ? [$tableIndex['flags']] : null;
196
        }
romanb's avatar
romanb committed
197

198
        return parent::_getPortableTableIndexesList($tableIndexRows, $tableName);
romanb's avatar
romanb committed
199 200 201
    }

    /**
202
     * {@inheritdoc}
romanb's avatar
romanb committed
203
     */
204
    protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey) : ForeignKeyConstraint
romanb's avatar
romanb committed
205
    {
206
        return new ForeignKeyConstraint(
207 208 209 210 211
            $tableForeignKey['local_columns'],
            $tableForeignKey['foreign_table'],
            $tableForeignKey['foreign_columns'],
            $tableForeignKey['name'],
            $tableForeignKey['options']
212
        );
romanb's avatar
romanb committed
213 214 215
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
216
     * {@inheritdoc}
romanb's avatar
romanb committed
217
     */
218
    protected function _getPortableTableDefinition(array $table) : string
romanb's avatar
romanb committed
219
    {
220 221 222 223
        if (isset($table['schema_name']) && $table['schema_name'] !== 'dbo') {
            return $table['schema_name'] . '.' . $table['name'];
        }

224
        return $table['name'];
romanb's avatar
romanb committed
225
    }
226 227

    /**
Benjamin Morel's avatar
Benjamin Morel committed
228
     * {@inheritdoc}
229
     */
230
    protected function _getPortableDatabaseDefinition(array $database) : string
231 232 233
    {
        return $database['name'];
    }
234

235 236 237
    /**
     * {@inheritdoc}
     */
238
    protected function getPortableNamespaceDefinition(array $namespace) : string
239 240 241 242
    {
        return $namespace['name'];
    }

243
    /**
Benjamin Morel's avatar
Benjamin Morel committed
244
     * {@inheritdoc}
245
     */
246
    protected function _getPortableViewDefinition(array $view) : View
247
    {
248
        // @todo
Sergei Morozov's avatar
Sergei Morozov committed
249
        return new View($view['name'], '');
250
    }
251

252
    /**
Benjamin Morel's avatar
Benjamin Morel committed
253
     * {@inheritdoc}
254
     */
255
    public function listTableIndexes(string $table) : array
256 257 258 259 260
    {
        $sql = $this->_platform->getListTableIndexesSQL($table, $this->_conn->getDatabase());

        try {
            $tableIndexes = $this->_conn->fetchAll($sql);
261 262
        } catch (PDOException $e) {
            if ($e->getCode() === 'IMSSP') {
263
                return [];
264
            }
265 266

            throw $e;
267
        } catch (DBALException $e) {
268
            if (strpos($e->getMessage(), 'SQLSTATE [01000, 15472]') === 0) {
269
                return [];
270
            }
271 272

            throw $e;
273 274 275 276
        }

        return $this->_getPortableTableIndexesList($tableIndexes, $table);
    }
277 278

    /**
Benjamin Morel's avatar
Benjamin Morel committed
279
     * {@inheritdoc}
280
     */
281
    public function alterTable(TableDiff $tableDiff) : void
282
    {
Steve Müller's avatar
Steve Müller committed
283 284
        if (count($tableDiff->removedColumns) > 0) {
            foreach ($tableDiff->removedColumns as $col) {
285 286
                $columnConstraintSql = $this->getColumnConstraintSQL($tableDiff->name, $col->getName());
                foreach ($this->_conn->fetchAll($columnConstraintSql) as $constraint) {
287 288 289 290 291 292 293
                    $this->_conn->exec(
                        sprintf(
                            'ALTER TABLE %s DROP CONSTRAINT %s',
                            $tableDiff->name,
                            $constraint['Name']
                        )
                    );
294 295 296 297
                }
            }
        }

Benjamin Morel's avatar
Benjamin Morel committed
298
        parent::alterTable($tableDiff);
299 300 301
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
302
     * Returns the SQL to retrieve the constraints for a given column.
303
     */
304
    private function getColumnConstraintSQL(string $table, string $column) : string
305 306 307 308 309 310
    {
        return "SELECT SysObjects.[Name]
            FROM SysObjects INNER JOIN (SELECT [Name],[ID] FROM SysObjects WHERE XType = 'U') AS Tab
            ON Tab.[ID] = Sysobjects.[Parent_Obj]
            INNER JOIN sys.default_constraints DefCons ON DefCons.[object_id] = Sysobjects.[ID]
            INNER JOIN SysColumns Col ON Col.[ColID] = DefCons.[parent_column_id] AND Col.[ID] = Tab.[ID]
311 312
            WHERE Col.[Name] = " . $this->_conn->quote($column) . ' AND Tab.[Name] = ' . $this->_conn->quote($table) . '
            ORDER BY Col.[Name]';
313
    }
314 315 316 317 318 319

    /**
     * Closes currently active connections on the given database.
     *
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
     */
320
    private function closeActiveDatabaseConnections(string $database) : void
321 322 323 324 325 326 327 328 329 330
    {
        $database = new Identifier($database);

        $this->_execSql(
            sprintf(
                'ALTER DATABASE %s SET SINGLE_USER WITH ROLLBACK IMMEDIATE',
                $database->getQuotedName($this->_platform)
            )
        );
    }
331

332
    public function listTableDetails(string $tableName) : Table
333 334 335 336 337 338 339 340 341
    {
        $table = parent::listTableDetails($tableName);

        /** @var SQLServerPlatform $platform */
        $platform = $this->_platform;
        $sql      = $platform->getListTableMetadataSQL($tableName);

        $tableOptions = $this->_conn->fetchAssoc($sql);

342 343 344
        if ($tableOptions !== false) {
            $table->addOption('comment', $tableOptions['table_comment']);
        }
345 346 347

        return $table;
    }
348
}