Completed
Push — 2.10 ( 39b76b...400f60 )
by Sergei
30s queued 15s
created

SQLServerSchemaManager   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 324
Duplicated Lines 0 %

Test Coverage

Coverage 91.37%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 52
eloc 127
c 1
b 0
f 0
dl 0
loc 324
ccs 127
cts 139
cp 0.9137
rs 7.44

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getPortableNamespaceDefinition() 0 3 1
A _getPortableDatabaseDefinition() 0 3 1
A _getPortableViewDefinition() 0 4 1
A _getPortableTableDefinition() 0 7 3
A _getPortableTableForeignKeysList() 0 23 3
A _getPortableTableForeignKeyDefinition() 0 8 1
A _getPortableTableIndexesList() 0 9 3
A dropDatabase() 0 23 4
A _getPortableSequenceDefinition() 0 3 1
A closeActiveDatabaseConnections() 0 8 1
F _getPortableTableColumnDefinition() 0 59 16
A parseDefaultExpression() 0 19 5
A alterTable() 0 18 4
A listTableIndexes() 0 21 5
A getColumnConstraintSQL() 0 8 1
A listTableDetails() 0 15 2

How to fix   Complexity   

Complex Class

Complex classes like SQLServerSchemaManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SQLServerSchemaManager, and based on these observations, apply Extract Interface, too.

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