Completed
Push — 2.10.x ( 61a6b9...f20ba1 )
by Grégoire
13:37 queued 11s
created

closeActiveDatabaseConnections()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1.0046

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 8
ccs 5
cts 6
cp 0.8333
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1.0046
1
<?php
2
3
namespace Doctrine\DBAL\Schema;
4
5
use Doctrine\DBAL\DBALException;
6
use Doctrine\DBAL\Driver\DriverException;
7
use Doctrine\DBAL\Platforms\SQLServerPlatform;
8
use Doctrine\DBAL\Types\Type;
9
use PDOException;
10
use Throwable;
11
use function assert;
12
use function count;
13
use function in_array;
14
use function is_string;
15
use function preg_match;
16
use function sprintf;
17
use function str_replace;
18
use function strpos;
19
use function strtok;
20
21
/**
22
 * SQL Server Schema Manager.
23
 */
24
class SQLServerSchemaManager extends AbstractSchemaManager
25
{
26
    /**
27
     * {@inheritdoc}
28
     */
29 155
    public function dropDatabase($database)
30
    {
31
        try {
32 155
            parent::dropDatabase($database);
33 155
        } catch (DBALException $exception) {
34 155
            $exception = $exception->getPrevious();
35 155
            assert($exception instanceof Throwable);
36
37 155
            if (! $exception instanceof DriverException) {
38
                throw $exception;
39
            }
40
41
            // If we have a error code 3702, the drop database operation failed
42
            // because of active connections on the database.
43
            // To force dropping the database, we first have to close all active connections
44
            // on that database and issue the drop database operation again.
45 155
            if ($exception->getErrorCode() !== 3702) {
46 155
                throw $exception;
47
            }
48
49 112
            $this->closeActiveDatabaseConnections($database);
50
51 112
            parent::dropDatabase($database);
52
        }
53 112
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58 110
    protected function _getPortableSequenceDefinition($sequence)
59
    {
60 110
        return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']);
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66 163
    protected function _getPortableTableColumnDefinition($tableColumn)
67
    {
68 163
        $dbType = strtok($tableColumn['type'], '(), ');
69 163
        assert(is_string($dbType));
70
71 163
        $fixed   = null;
72 163
        $length  = (int) $tableColumn['length'];
73 163
        $default = $tableColumn['default'];
74
75 163
        if (! isset($tableColumn['name'])) {
76 26
            $tableColumn['name'] = '';
77
        }
78
79 163
        if ($default !== null) {
80 141
            $default = $this->parseDefaultExpression($default);
81
        }
82
83 1
        switch ($dbType) {
84 163
            case 'nchar':
85 163
            case 'nvarchar':
86 163
            case 'ntext':
87
                // Unicode data requires 2 bytes per character
88 137
                $length /= 2;
89 137
                break;
90
91 163
            case 'varchar':
92
                // TEXT type is returned as VARCHAR(MAX) with a length of -1
93 116
                if ($length === -1) {
94 116
                    $dbType = 'text';
95
                }
96
97 116
                break;
98
        }
99
100 163
        if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') {
101 102
            $fixed = true;
102
        }
103
104 163
        $type                   = $this->_platform->getDoctrineTypeMapping($dbType);
105 163
        $type                   = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
106 163
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
107
108
        $options = [
109 163
            'length'        => $length === 0 || ! in_array($type, ['text', 'string']) ? null : $length,
110
            'unsigned'      => false,
111 163
            'fixed'         => (bool) $fixed,
112 163
            'default'       => $default,
113 163
            'notnull'       => (bool) $tableColumn['notnull'],
114 163
            'scale'         => $tableColumn['scale'],
115 163
            'precision'     => $tableColumn['precision'],
116 163
            'autoincrement' => (bool) $tableColumn['autoincrement'],
117 163
            'comment'       => $tableColumn['comment'] !== '' ? $tableColumn['comment'] : null,
118
        ];
119
120 163
        $column = new Column($tableColumn['name'], Type::getType($type), $options);
121
122 163
        if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') {
123 159
            $column->setPlatformOption('collation', $tableColumn['collation']);
124
        }
125
126 163
        return $column;
127
    }
128
129 141
    private function parseDefaultExpression(string $value) : ?string
130
    {
131 141
        while (preg_match('/^\((.*)\)$/s', $value, $matches)) {
132 141
            $value = $matches[1];
133
        }
134
135 141
        if ($value === 'NULL') {
136
            return null;
137
        }
138
139 141
        if (preg_match('/^\'(.*)\'$/s', $value, $matches)) {
140 139
            $value = str_replace("''", "'", $matches[1]);
141
        }
142
143 141
        if ($value === 'getdate()') {
144 102
            return $this->_platform->getCurrentTimestampSQL();
145
        }
146
147 141
        return $value;
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153 141
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
154
    {
155 141
        $foreignKeys = [];
156
157 141
        foreach ($tableForeignKeys as $tableForeignKey) {
158 124
            if (! isset($foreignKeys[$tableForeignKey['ForeignKey']])) {
159 124
                $foreignKeys[$tableForeignKey['ForeignKey']] = [
160 124
                    'local_columns' => [$tableForeignKey['ColumnName']],
161 124
                    'foreign_table' => $tableForeignKey['ReferenceTableName'],
162 124
                    'foreign_columns' => [$tableForeignKey['ReferenceColumnName']],
163 124
                    'name' => $tableForeignKey['ForeignKey'],
164
                    'options' => [
165 124
                        'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']),
166 124
                        'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']),
167
                    ],
168
                ];
169
            } else {
170 56
                $foreignKeys[$tableForeignKey['ForeignKey']]['local_columns'][]   = $tableForeignKey['ColumnName'];
171 56
                $foreignKeys[$tableForeignKey['ForeignKey']]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName'];
172
            }
173
        }
174
175 141
        return parent::_getPortableTableForeignKeysList($foreignKeys);
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     */
181 141
    protected function _getPortableTableIndexesList($tableIndexRows, $tableName = null)
182
    {
183 141
        foreach ($tableIndexRows as &$tableIndex) {
184 114
            $tableIndex['non_unique'] = (bool) $tableIndex['non_unique'];
185 114
            $tableIndex['primary']    = (bool) $tableIndex['primary'];
186 114
            $tableIndex['flags']      = $tableIndex['flags'] ? [$tableIndex['flags']] : null;
187
        }
188
189 141
        return parent::_getPortableTableIndexesList($tableIndexRows, $tableName);
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 124
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
196
    {
197 124
        return new ForeignKeyConstraint(
198 124
            $tableForeignKey['local_columns'],
199 124
            $tableForeignKey['foreign_table'],
200 124
            $tableForeignKey['foreign_columns'],
201 124
            $tableForeignKey['name'],
202 124
            $tableForeignKey['options']
203
        );
204
    }
205
206
    /**
207
     * {@inheritdoc}
208
     */
209 155
    protected function _getPortableTableDefinition($table)
210
    {
211 155
        if (isset($table['schema_name']) && $table['schema_name'] !== 'dbo') {
212 78
            return $table['schema_name'] . '.' . $table['name'];
213
        }
214
215 155
        return $table['name'];
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221 112
    protected function _getPortableDatabaseDefinition($database)
222
    {
223 112
        return $database['name'];
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229 104
    protected function getPortableNamespaceDefinition(array $namespace)
230
    {
231 104
        return $namespace['name'];
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237 76
    protected function _getPortableViewDefinition($view)
238
    {
239
        // @todo
240 76
        return new View($view['name'], '');
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246 141
    public function listTableIndexes($table)
247
    {
248 141
        $sql = $this->_platform->getListTableIndexesSQL($table, $this->_conn->getDatabase());
249
250
        try {
251 141
            $tableIndexes = $this->_conn->fetchAll($sql);
252
        } catch (PDOException $e) {
253
            if ($e->getCode() === 'IMSSP') {
254
                return [];
255
            }
256
257
            throw $e;
258
        } catch (DBALException $e) {
259
            if (strpos($e->getMessage(), 'SQLSTATE [01000, 15472]') === 0) {
260
                return [];
261
            }
262
263
            throw $e;
264
        }
265
266 141
        return $this->_getPortableTableIndexesList($tableIndexes, $table);
267
    }
268
269
    /**
270
     * {@inheritdoc}
271
     */
272 122
    public function alterTable(TableDiff $tableDiff)
273
    {
274 122
        if (count($tableDiff->removedColumns) > 0) {
275 122
            foreach ($tableDiff->removedColumns as $col) {
276 122
                $columnConstraintSql = $this->getColumnConstraintSQL($tableDiff->name, $col->getName());
277 122
                foreach ($this->_conn->fetchAll($columnConstraintSql) as $constraint) {
278 122
                    $this->_conn->exec(
279 122
                        sprintf(
280
                            'ALTER TABLE %s DROP CONSTRAINT %s',
281 122
                            $tableDiff->name,
282 122
                            $constraint['Name']
283
                        )
284
                    );
285
                }
286
            }
287
        }
288
289 122
        parent::alterTable($tableDiff);
290 122
    }
291
292
    /**
293
     * Returns the SQL to retrieve the constraints for a given column.
294
     *
295
     * @param string $table
296
     * @param string $column
297
     *
298
     * @return string
299
     */
300 122
    private function getColumnConstraintSQL($table, $column)
301
    {
302
        return "SELECT SysObjects.[Name]
303
            FROM SysObjects INNER JOIN (SELECT [Name],[ID] FROM SysObjects WHERE XType = 'U') AS Tab
304
            ON Tab.[ID] = Sysobjects.[Parent_Obj]
305
            INNER JOIN sys.default_constraints DefCons ON DefCons.[object_id] = Sysobjects.[ID]
306
            INNER JOIN SysColumns Col ON Col.[ColID] = DefCons.[parent_column_id] AND Col.[ID] = Tab.[ID]
307 122
            WHERE Col.[Name] = " . $this->_conn->quote($column) . ' AND Tab.[Name] = ' . $this->_conn->quote($table) . '
308
            ORDER BY Col.[Name]';
309
    }
310
311
    /**
312
     * Closes currently active connections on the given database.
313
     *
314
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
315
     *
316
     * @param string $database The name of the database to close currently active connections for.
317
     *
318
     * @return void
319
     */
320 112
    private function closeActiveDatabaseConnections($database)
321
    {
322 112
        $database = new Identifier($database);
323
324 112
        $this->_execSql(
325 112
            sprintf(
326
                'ALTER DATABASE %s SET SINGLE_USER WITH ROLLBACK IMMEDIATE',
327 112
                $database->getQuotedName($this->_platform)
328
            )
329
        );
330 112
    }
331
332
    /**
333
     * @param string $tableName
334
     */
335 141
    public function listTableDetails($tableName) : Table
336
    {
337 141
        $table = parent::listTableDetails($tableName);
338
339 141
        $platform = $this->_platform;
340 141
        assert($platform instanceof SQLServerPlatform);
341 141
        $sql = $platform->getListTableMetadataSQL($tableName);
342
343 141
        $tableOptions = $this->_conn->fetchAssoc($sql);
344
345 141
        if ($tableOptions !== false) {
346 8
            $table->addOption('comment', $tableOptions['table_comment']);
347
        }
348
349 141
        return $table;
350
    }
351
}
352